数据结构和算法总结(二):排序
前言
复习各种排序算法,并记录下。
正文
1.冒泡排序
冒泡可以说是最简单的排序算法,它的排序过程就是每次遍历数组将最大的那个数往前顶,就好像气泡上浮一样。
过程可以参考如下图
参考代码
void bubbleSort(vector<int>& num) { for(int i = num.size()- 1;i > 0;i--) { for(int j = 1;j <= i;j++) { if(num[j] < num[j-1]) swap(num[j],num[j-1]); } } }
复杂度分析
冒泡的最坏情况下的时间复杂度为:O(n2),平均复杂度:O(n2) 。
优化
我们可以稍微优化下冒泡排序,增加一个标识来确定一个数组是否有序,如果是那么可以提前终止排序,参考代码如下
//提前终止的冒泡排序 bool PreStopBubble(vector<int>& num,const int n) { bool isSwapped = false; for(int i = 1;i < n;i++) { if(num[i - 1] > num[i]) { isSwapped = true; cout << "Swapped!" << endl; swap(num[i - 1],num[i]); } } return isSwapped; } void PreStopBubbleSort(vector<int>&num) { for(int i = num.size();i > 0 && PreStopBubble(num,i);i--); //如果某一次冒泡过程未发生交换,那么说明数组已经有序,所以提前终止 }
这种情形下,冒泡排序的最好复杂度可以达到O(n)。
2.选择排序
选择排序和冒泡排序的思路类似,都是找到最大(或者最小)的数,只不过选择排序需要额外的空间来存储一次遍历过程中最大(或者最小)的数字和它的位置,然后将它与首位(或者末位)交换。
过程可以参考如下图
注:图中的选择排序每次是选择的最小值,参考代码每次是选取最大值。
参考代码
void SelectionSort(vector<int>& num) { for(int i = num.size();i > 0 ;i--) { int tmp = num[0],index = 0; for(int j = 1;j < i;j++) { if(num[j] > tmp) { tmp = num[j]; index = j; } } swap(num[index],num[i - 1]); } }
复杂度分析
选择排序的最坏情况下的时间复杂度为:O(n2),平均复杂度:O(n2) 。但是往往选择排序效率优于冒泡排序(如果不是提前终止的冒泡),因为每一次遍历选择排序只需要发生一次交换,而冒泡排序可能发生了多次交换,然而,这是选择排序牺牲了额外空间来存储最值得来的。
3.计数排序
计数排序,也叫桶排序。这种排序方法适合已知一定范围内的数字排序,每一个数字都有一个对应的桶,一次遍历将数组的数字放入到对应的桶中进行统计,然后遍历每个桶将其输出即可。
过程可以参考如下图
参考代码
void CountSort(vector<int>& num,const int maxnum) { vector<int> countBox(maxnum + 1,0); for(int i =0;i < num.size();i++) { countBox[num[i]]++; } int j = 0; for(int i = 0;i < num.size();i++) { if(countBox[j]-- > 0) num[i] = j; else { j++;i--; } } }
复杂度分析
计数排序的平均时间复杂度为:O(n)。但是它的空间复杂度很差。这是一种牺牲空间换取时间的排序算法。
4.归并排序
有序数组的合并
首先,我们要知道如何将两个有序数组合并。方法很简单,从头开始比较两个数组的数字,用一个额外的数组tmp存储一次比较时的较小数字,然后较小者所在数组的索引向后+1继续和之前另一个数组的较大者比较。如果其中一个数组遍历到末尾,那么把另一个数组的剩余元素依次添加到tmp数组末尾即可,这样tmp就是一个合并后的有序数组。
分治
而对于一个无序数组,我们可以将它划分为两个无序子数组,子数组又可以不停划分,直到当一个子数组只有一个数时,这时这个子数组肯定是有序的,那么我们就可以将一个个有序子数组合并成更大的有序子数组,直到最终合并成一个有序数组,这就是归并排序的思想。
排序过程可以参考如下图
参考代码
void MergeArray(vector<int>& num,int left,int right,int mid,vector<int>& tmp) { //合并两个有序子数组 int l = left,lm = mid; int mr = mid + 1,r = right; int k = 0; while(l <= lm && mr <= r) { if(num[l] < num[mr]) tmp[k++] = num[l++]; else tmp[k++] = num[mr++]; } while(l <= lm) tmp[k++] = num[l++]; while(mr <= r) tmp[k++] = num[mr++]; for(int i = 0;i < k;i++) { num[left + i] = tmp[i]; } } void MergeSort(vector<int>& num,int left,int right,vector<int>& tmp) { if(left >= right) return; int mid = (left + right)/2; //递归+分治 MergeSort(num,left,mid,tmp); MergeSort(num,mid + 1,right,tmp); MergeArray(num,left,right,mid,tmp); } void MergeSort_begin(vector<int>& num) //归并排序入口 { if(num.empty()) return ; vector<int> tmp(num.size()); MergeSort(num,0,num.size() - 1,tmp); }
复杂度分析
归并排序的最坏时间复杂度为:O(nlogn),平均复杂度为:O(nlogn)。
5.快速排序
快速排序的思想从本质来说与归并排序类似,也是分治。只不过快速排序是在数组中选定了一个轴点,比轴点小的数字划分到左边,比轴点大的划分到右边,这样一个数组就被划分成了两部分,然后在这两部分基础上继续选择一个轴点划分,如此直到无法划分为止。
排序过程可以参考如下图
参考代码
注:这里是选取每个数组最左边的数字为轴点。
int qs_partition(vector<int>& num,int left,int right) { int pivot = num[left]; int l = left,r =right; while(l < r) { while(l < r && num[r] >= pivot) r--; if(l < r) num[l++] = num[r]; while(l < r && num[l] < pivot) l++; if(l < r) num[r--] = num[l]; } num[l] = pivot; return l; } void quickSort(vector<int>& num,int left,int right) { if(left > right) return; int k = qs_partition(num,left,right); quickSort(num,left,k - 1); quickSort(num,k + 1,right); }
上述的为递归的快速排序,如果数据量非常大可能会导致栈内存爆掉,所以可以用一个栈来实现非递归的快速排序。
非递归的快速排序参考代码
int qs_partition(vector<int>& num,int left,int right) { int pivot = num[left]; int l = left,r =right; while(l < r) { while(l < r && num[r] >= pivot) r--; if(l < r) num[l++] = num[r]; while(l < r && num[l] < pivot) l++; if(l < r) num[r--] = num[l]; } num[l] = pivot; return l; } void stack_quickSort(vector<int>& num) { int left = 0,right = num.size() - 1; if(left > right) return; stack<int> stk; int l,r; stk.push(right); stk.push(left); while(!stk.empty()) { l = stk.top();stk.pop(); r = stk.top();stk.pop(); if(l < r) { int k = qs_partition(num,l,r); stk.push(k - 1);stk.push(l); stk.push(r);stk.push(k + 1); } } }
复杂度分析
快速排序的最坏时间复杂度为:O(n2),即轴点的左侧或者右侧没有数字。最好的情况是左右两侧数字大致相同,平均复杂度为:O(nlogn)。
补充
快速排序的轴点选择对于该排序算法的效率有很大的影响,最普通的选取最左或者最右的数字作为轴点的方法其实不太稳定,常用的选取轴点的方法是随机取值或者三值取中。
三值取中:顾名思义,在最左、最右、中间三个位置选取三个数字,然后在三个数字中选取值居于中间的那个数字作为轴点。
参考资料
《数据结构、算法与应用——C++描述》 作者:【美】 萨特吉·萨尼 机械工业出版社