常见排序算法及其C++实现

对常见排序算法进行总结,下图可做参考:

在介绍排序算法之前先明确几个概念:

1、时间复杂度:从序列的初始状态到最终排序好的结果状态所花费的时间度量,主要取决于变换、移位等操作语句的执行次数。

2、空间复杂度:从序列的初始状态到最终状态所花费的空间开销,即排序过程中所需要开辟的新的内存空间。

3、稳定性:排序算法是否会改变大小相等元素的相对次序,比如在原序列中r[i]=r[j]、且r[i]在r[j]之前,而排序后r[i]仍在r[j]之前,则该排序算法是稳定的。

4、内部外部排序:内部排序是指不需要开辟新的数组空间,在原先序列的内存中即可完成排序;而外部排序需要借助新的内存空间。

下面依次介绍,并贴上自己的C++实现代码(以从小到大排列为例):

一、冒泡排序

原理:从前往后访问要排序的序列,每次比较相邻两个元素,若相对位置不符合排序要求,则将它们互换。

需要两层循环实现,外层循环表示排序的趟数,n个数据需要n-1趟,外层每次循环结束都会有一个最大的元素“下沉”到序列尾,而小元素则随着比较慢慢“上浮”,这也是冒泡排序名称的来由。

内层循环表示每趟需要排序的元素,m趟排完后就会有m个元素已经有序,不参与m+1趟排序。

时间复杂度:平均为O(n2)、最好情况为O(n) 、最坏情况为O(n2)

空间复杂度:不需要开辟大量新的内存,仅需一个地址用于交换数据,为O(1)

实现代码:

 1 //交换元素
 2 void Swap(int &a, int &b)
 3 {
 4     int useValue = 0;
 5     useValue = a;
 6     a = b;
 7     b = useValue;
 8 }
 9 //冒泡排序
10 void BubbleSort(vector<int> &arr)
11 {
12     for (int i = 0; i < arr.size() - 1; i++)
13     {
14         for (int j = 0; j < arr.size() -1 - i; j++)
15         {
16             if (arr[j] > arr[j + 1]) Swap(arr[j + 1], arr[j]);
17         }
18     }
19 }
20 int main()
21 {
22     vector<int> arr = {5, 5, 6, 12, 1, 12, 23, 19, 20, 24, 19, 19, 19, 19, 15, 15, 3, -2};
23     BubbleSort(arr);   //冒泡排序
24     for (auto ele : arr) cout << ele << " ";
25     cout << endl;
26     return 0;
27 }

二、选择排序

原理:遍历序列找到当前序列中最小元素的位置并记录,将其与序列最前面元素交换,得到一部分有序序列,下次遍历仅遍历后面无序的序列,直到所有元素均有序。这种排序方法避免了冒泡排序中数据交换次数过多的问题。

也需要两层循环实现,每趟排序使最小的元素上浮到本次排序的最前面,下一趟排序时,已经有序的元素不再参与。

时间复杂度:因为不管是否序列已有序,都要比较并记录位置,故平均情况、最好情况、最坏情况均为O(n2)

时间复杂度:O(1)

实现代码:

 1 //交换元素
 2 void Swap(int &a, int &b)
 3 {
 4     int useValue = 0;
 5     useValue = a;
 6     a = b;
 7     b = useValue;
 8 }
 9 //选择排序
10 void SelectSort(vector<int> &arr)
11 {
12     for (int i = 0; i < arr.size(); i++)
13     {
14         int location = i;
15         for (int j = i + 1; j < arr.size(); j++)
16         {
17             if (arr[j] < arr[location]) location = j;
18         }
19         Swap(arr[i], arr[location]);
20     }
21 }
22 int main()
23 {
24     vector<int> arr = {5, 5, 6, 12, 1, 12, 23, 19, 20, 24, 19, 19, 19, 19, 15, 15, 3, -2};
25     SelectSort(arr);      //选择排序
26     for (auto ele : arr) cout << ele << " ";
27     cout << endl;
28     return 0;
29 }

三、插入排序

 原理:构建有序序列,对于未排序的无序部分,遍历有序部分并找合适的地方插入,直到所有元素均已有序。

需要两层循环,外层循环表示比较的趟数,排完m趟就会有m个元素已经有序,并放置于序列前面部分,下一趟会遍历这m个元素,并将m+1个元素插入到它们中合适的位置;内层循环表示这一趟已经有序元素的个数。

插入排序可以借助外部内存实现,那样更好理解一点,但是也可以直接内部排序。

时间复杂度:平均为O(n2)、最好情况为O(n) 、最坏情况为O(n2)

空间复杂度:O(1)

实现代码:

 1 //插入排序
 2 void InsertSort(vector<int> &arr)
 3 {
 4     for (int i = 0; i < arr.size(); i++)
 5     {
 6         int j, tmp = arr[i];
 7         for (j = i; j > 0; j--)
 8         {
 9             if (arr[j - 1] <= tmp) break;
10             else arr[j] = arr[j - 1];
11         }
12         arr[j] = tmp;
13     }
14 }
15 int main()
16 {
17     vector<int> arr = {5, 5, 6, 12, 1, 12, 23, 19, 20, 24, 19, 19, 19, 19, 15, 15, 3, -2};
18     InsertSort(arr);    //插入排序
19     for (auto ele : arr) cout << ele << " ";
20     cout << endl;
21     return 0;
22 }

四、希尔排序

原理:加强版的插入排序,先将整个待排序的序列分割成为若干子序列,分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。避免了插入排序每次只能将数据移动一位的缺陷。

需要设定一个希尔序列增量t,如果序列长度为n,可以设置t的初始值为n/2,但t是递减的,需要设定递减规则但最终t必须要递减到1,可以设置规则为t=t/2。对每一个t值,都会有若干组数据在进行插入排序,当t=1时,整个序列已经基本有序,再排一趟就可以完成整体排序。

举个例子:对12,3,16,8,9,14,1,36,9,11这十个数排序

当t=n/2=5时,对12,14;3,1;16,36;8,9;9,11这五组序列插入排序(增量为5),得到12,1,16,8,9,14,3,36,9,11;

当t=t/2=2时,对12,16,9,3,9和1,8,14,36,11这两组序列插入排序(增量为2),得到3,1,9,8,9,11,12,14,16,36;

当t=t/2=1时,直接对上一步得到的序列插入排序(增量为1),得到1,3,8,9,9,11,12,14,16,36。

时间复杂度:希尔排序的时间复杂度计算很复杂,平均可认为是O(nlogn)即可,最好情况为O(n) 、最坏情况为O(n2)。最上面的表格中可能有错误。

空间复杂度:O(1)

实现代码:

 1 //希尔排序
 2 void ShellSort(vector<int> &arr)
 3 {
 4     int shellValue = arr.size() / 2;  //定义希尔值
 5     while (shellValue >= 1)
 6     {
 7         for (int i = shellValue; i < arr.size(); i++)
 8         {
 9             int j, tmp = arr[i];
10             for (j = i; j / shellValue > 0; j = j - shellValue)
11             {
12                 if (tmp >= arr[j - shellValue]) break;
13                 else arr[j] = arr[j - shellValue];
14             }
15             arr[j] = tmp;
16         }
17         shellValue = shellValue / 2;
18     }
19 }
20 int main()
21 {
22     vector<int> arr = {5, 5, 6, 12, 1, 12, 23, 19, 20, 24, 19, 19, 19, 19, 15, 15, 3, -2};
23     ShellSort(arr);       //希尔排序
24     for (auto ele : arr) cout << ele << " ";
25     cout << endl;
26     return 0;
27 }

五、归并排序

原理:采用分治法的原理,建立在归并操作的基础上。一般是自上而下的递归,将初始序列分为两半,两半再分为四份,四份分为八份.......直到每一份仅含一个元素,然后两个相邻的元素比较,按大小排序得到一组,一组再与相邻的组归并得到更大的组,更大的组再与其他组归并,直到所有元素均已有序。

举个例子表示归并的方式:归并2,8,14,20,21和1,9,15两组序列,需要先开辟新的内存空间,然后按大小顺序将元素放入,先放1,再放2,8,接着是14..........直到得到1,2,8,9,14,15,20,21完成归并操作。

归并排序需要开辟新的内存,所以是一种外部排序。

时间复杂度:递归过程可看作利用了二叉树结构,共需要nlogn次完成分割,每次比较N次,故平均情况、最好情况、最坏情况均为O(nlogn)

空间复杂度:O(n)

实现代码:

 1 //归并排序
 2 vector<int> MergeSort(vector<int> &arr)
 3 {
 4     if (arr.size() < 2) return arr;
 5     else
 6     {
 7         int middle = arr.size() / 2;
 8         return Merge(MergeSort(vector<int>(arr.begin(), arr.begin() + middle)), MergeSort(vector<int>(arr.begin() + middle, arr.end())));
 9     }
10 }
11 //归并排序中具体归并过程
12 vector<int> Merge(vector<int> left, vector<int> right)
13 {
14     vector<int> result;
15     vector<int>::iterator leftVec = left.begin(), rightVec = right.begin();
16     while (leftVec != left.end() && rightVec != right.end())
17     {
18         result.push_back((*leftVec <= *rightVec) ? (*leftVec++) : (*rightVec++));
19     }
20     while (leftVec != left.end()) result.push_back(*leftVec++);
21     while (rightVec != right.end()) result.push_back(*rightVec++);
22     return result;
23 }
24 int main()
25 {
26     vector<int> arr = {5, 5, 6, 12, 1, 12, 23, 19, 20, 24, 19, 19, 19, 19, 15, 15, 3, -2};
27     vector<int> result = MergeSort(arr);      //归并排序
28     for (auto ele : result) cout << ele << " ";
29     cout << endl;
30     return 0;
31 }

六、快速排序

原理:在元素中挑出一个“基准”元素,拿其他元素与之比较,将基准放在中间,比基准小的都放在左边,大的都放在右边,然后递归对基准左、右边序列再进行这样的排序,直到所有元素有序。

快速排序也是“分治法”的一种实现,它是平均情况下最快、效率最高的排序算法,虽然最坏状况下需要 Ο(n2) 次比较,但这种状况并不常见。快排通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来,而且O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。

时间复杂度:平均为O(nlogn)、最好情况为O(nlogn) 、最坏情况为O(n2)

空间复杂度:需要记录递归过程,故空间复杂度为递归深度O(logn)

实现代码:(通常情况下由多个函数辅助实现,但也可以由一个函数单个实现)

 1 //交换元素
 2 void Swap(int &a, int &b)
 3 {
 4     int useValue = 0;
 5     useValue = a;
 6     a = b;
 7     b = useValue;
 8 }
 9 //快速排序
10 void QuickSort(vector<int> &arr, int left, int right)
11 {
12     if (right - left >= 0)
13     {
14         int midValue = arr[left - 1];
15         int midLocation = left - 1;
16         int rightLocation = right;
17 
18         while (left != right)
19         {
20             while (arr[left] <= midValue && left < right) left++;
21             while (arr[right] >= midValue && left != right) right--;
22             Swap(arr[left], arr[right]);
23         }
24         if (arr[left] >= midValue) left--; right--;
25         if (arr[left] < midValue) Swap(arr[midLocation], arr[left]);
26         QuickSort(arr, midLocation + 1, left - 1);
27         QuickSort(arr, left + 2, rightLocation);
28     }
29 }
30 int main()
31 {
32     vector<int> arr = {5, 5, 6, 12, 1, 12, 23, 19, 20, 24, 19, 19, 19, 19, 15, 15, 3, -2};
33     QuickSort(arr, 1, arr.size() - 1);  //快速排序
34     for (auto ele : arr) cout << ele << " ";
35     cout << endl;
36     return 0;
37 }

七、堆排序

 原理:要想介绍堆排序的原理,就需要先介绍什么是堆及堆的常见操作。

在数据结构中有一种树结构称为完全二叉树:若设二叉树的深度为n,则除第n层外其他各层的节点数都达到最大个数,且第n层上的所有节点都集中在最左侧。

堆就是完全二叉树,且一般情况下堆分为最大堆和最小堆,最大堆中每个父节点元素都大于或等于两个子节点,最小堆则相反。所以最大堆中根节点就是整个序列中的最大值,最小堆中是最小值。

比如下图就是一个最小堆:

堆可以用数组结构来存储,按从上到下、从左到右的顺序,对应于数组的下标编号。节点i的左子节点为2i+1,右子节点为2i+2,父节点为(i-1)/2。

对于堆,常见的操作有上浮、下沉、插入、弹出、取顶和堆排序,本文仅介绍堆排序,想了解堆其他操作的可以看这篇:https://www.cnblogs.com/schips/p/10660163.html

堆排序的思路其实很简单,如果想从小到大排序,则构建最大堆,首先对全部n个元素构建最大堆,此时根节点为最大值,将根节点取出与最后一个节点交换,然后最后的节点不再参与运算,对剩下的n-1个元素构建堆,依次操作,直到所有元素有序。

堆排序中关键点在于构建堆的过程,可以理解为从最后一个父节点开始,依次往上对每个节点进行下沉。

时间复杂度:平均情况、最好情况、最坏情况均为O(nlogn)

空间复杂度:O(1)

实现代码:(构建堆过程中下沉操作可以用while循环实现,这里我使用了递归方式)

 1 //交换元素
 2 void Swap(int &a, int &b)
 3 {
 4     int useValue = 0;
 5     useValue = a;
 6     a = b;
 7     b = useValue;
 8 }
 9 //堆排序
10 void HeapSort(vector<int> &arr)
11 {
12     int sizeBegin = arr.size();
13     for (int i = 0; i < sizeBegin; i++)
14     {
15         BuildHeap(arr, sizeBegin - i);  //构建最大堆
16         Swap(arr[0], arr[sizeBegin - 1 - i]);
17     }
18 }
19 //构建最大堆
20 //参数为序列的引用和序列长度
21 void BuildHeap(vector<int> &arr, int size)
22 {
23     if (size > 1)  //仅当序列中有至少两个元素时才构建堆
24     {
25         for (int i = size / 2 - 1; i >= 0; i--) AdjustHeap(arr, size, i);
26     }
27 }
28 //调整堆
29 //参数为序列的引用、序列长度和要调整节点的位置
30 void AdjustHeap(vector<int> &arr, int size, int location)
31 {
32     int leftChild = location * 2 + 1;  //左子节点
33     int rightChild = location * 2 + 2;   //右子节点
34     if (rightChild < size)   //表示该节点既有左子节点,也有右子节点
35     {
36         int maxChild = arr[leftChild] > arr[rightChild] ? arr[leftChild] : arr[rightChild];
37         if (arr[location] < maxChild)
38         {
39             if (maxChild == arr[leftChild])
40             {
41                 Swap(arr[location], arr[leftChild]);
42                 AdjustHeap(arr, size, leftChild);
43             }
44             else
45             {
46                 Swap(arr[location], arr[rightChild]);
47                 AdjustHeap(arr, size, rightChild);
48             }
49         }
50     }
51     else if (leftChild < size)   //只有左子节点
52     {
53         if (arr[location] < arr[leftChild]) Swap(arr[location], arr[leftChild]);
54     }
55 }
56 int main()
57 {
58     vector<int> arr = {5, 5, 6, 12, 1, 12, 23, 19, 20, 24, 19, 19, 19, 19, 15, 15, 3, -2};
59     HeapSort(arr);      //堆排序
60     for (auto ele : arr) cout << ele << " ";
61     cout << endl;
62     return 0;
63 }

八、交换排序

原理:类似于选择排序和冒泡排序,由两层循环完成,外部循环表示排序的趟数,每排一趟找出最小值放到最前面,依次进行直到所有元素有序。

以前经常将这种方法与冒泡排序混淆,其实两者还是有区别的,冒泡排序是相邻的两个数比较并交换数据,而交换排序是用最前面的元素与所有元素比较,如果顺序不符合要求就交换。

与选择排序也很类似,而选择排序省略了数据交换的过程,交换排序需要很多数据交换操作。

时间复杂度:平均为O(n2)、最好情况为O(n) 、最坏情况为O(n2)

空间复杂度:O(1)

实现代码:

 1 //交换元素
 2 void Swap(int &a, int &b)
 3 {
 4     int useValue = 0;
 5     useValue = a;
 6     a = b;
 7     b = useValue;
 8 }
 9 //交换排序
10 void ExchangeSort(vector<int> &arr)
11 {
12     for (int i = 0; i < arr.size() - 1; i++)
13     {
14         for (int j = i + 1; j < arr.size(); j++)
15         {
16             if (arr[i] > arr[j]) Swap(arr[i], arr[j]);
17         }
18     }
19 }
20 int main()
21 {
22     vector<int> arr = {5, 5, 6, 12, 1, 12, 23, 19, 20, 24, 19, 19, 19, 19, 15, 15, 3, -2};
23     ExchangeSort(arr);  //交换排序
24     for (auto ele : arr) cout << ele << " ";
25     cout << endl;
26     return 0;
27 }

除了以上几种排序,常见的还有计数排序、桶排序与基数排序,但应用场景可能比较局限,就不做介绍。感兴趣的可以参考这个链接:https://www.runoob.com/w3cnote/ten-sorting-algorithm.html

本文参考的博文除了以上两个外,还有以下链接,表示感谢:

https://blog.csdn.net/m0_37962600/article/details/81475585

https://blog.csdn.net/weixin_43956598

原文地址:https://www.cnblogs.com/MatthewHome/p/12459662.html