排序
参考:常用排序及过程可视化 图解快速排序 图解堆排序 八大排序算法
注意:比较排序数学意义上的最优时间复杂度为nlgn
stl提供的排序算法有sort,可以自定义比较函数,假设有一个student类,要根据学号成绩排序,student和int不同,没有一定需要重载<操作符的必要,即使重载,也应该是比较学号。解决办法是定义一个全局的比较函数,传递给sort(s.begin(),s.end(), foo);这儿不能传递仿函数less<student>,因为less也是对student对象作<操作(如果student重载的<是按学号比较的)。仿函数是类内重载了()操作符,传递给sort的是个临时对象,sort(s.begin(),s.end(), less<int>())。仿函数比全局比较函数好的地方在于没有全局的变量,仿函数本身是类,可以保存对象状态。 STL仿函数
部分排序partial_sort(v.begin(),v.begin()+3,v.end(),greater<int>()) 可以对0-3部分进行降序排列,得到最大的前3个数。
nth_element(vect.begin(), vect.begin()+3, vect.end(),less<student>()); 得到倒数第4名成绩的学生。
用仿函数或者自定义全局比较函数以及重载<操作的方式都有缺点,如程序显得杂乱,sort的第三个参数是谓词,lambda表达式也是谓词,sort(a,a+n,[](int x,int y){return x<y})。
冒泡排序:在一趟比较交换过程中没有发生交换,则序列已经有序。可以从前往后比较交换,或者从后向前compare and swap;O(n^2)的时间复杂度已经很高,线性时间复杂度O(n)<O(nlogn)<O(n^2),最小的是常数时间——不随n的规模扩大而变化;如果交换条件改为A[j] > =A[j +1] ,会变化为unstable sort.
时间复杂度分析:1)最坏情况,待排序列逆序,内层循环每次都发生交换,交换次数为n-i(外层循环发生n-1次),总次数为等差数列的和:(n-1+1)*(n-1)/2=n(n-1)/2,时间复杂度为O(n^2) ;2)最好情况,序列有序,内层循环执行n-1次比较后(不执行交换)就结束了,时间复杂度为O(n);所以平均时间复杂度为O(n^2)。
输入数组为:int a[] = { 2, 6, 95, 21, 35, 12, 11, 3, 78, 21, 14, 0 };
1 void bubllesort(int arr[],int n) //从头向后比较交换位置,降序排列 2 { 3 for (int i = 0; i < n; i++) 4 { 5 bool change = false; 6 for (int j = 0; j < n - i-1; j++) 7 { 8 if (arr[j] < arr[j + 1]) { 9 std::swap(arr[j], arr[j + 1]); change = true; 10 } 11 } 12 if (change == false) break;//如果一趟没有发生交换,则已经全部有序,需要中断循环 13 } 14 } 15 void bublesort2(int A[], int n)//从后向前冒泡 16 { 17 for (int i = 0; i < n-1; i++) //控制比较趟数 18 { 19 bool flag = false; 20 for (int j = n - 1; j > i; j--) //每一趟的比较 21 { 22 if (A[j] > A[j - 1]){ 23 std::swap(A[j], A[j - 1]); flag = true; 24 } // 如果条件改成A[i] >= A[i + 1],则变为不稳定的排序算法 25 } 26 if (flag == false) return; 27 } 28 } 29 30 BubbleSort
选择排序:从头开始,每次选择最小的元素,交换最小的和第一个的位置;选择排序是不稳定的排序算法,不稳定发生在最小元素与A[i]交换的时刻。比如序列:{ 5, 8, 5, 2, 9 },一次选择的最小元素是2,然后把2和第一个5进行交换,从而改变了两个元素5的相对次序;
1 void selectionsort(int A[], int n) 2 { 3 for (int i = 0; i < n-1; i++) 4 { 5 int min = A[i]; 6 int index = i; //最小元素的索引 7 for (int j = i+1; j < n; j++)//从A[i]下一个元素开始比较 8 { 9 if (A[j] < min) { min = A[j]; index = j; } 10 11 12 } 13 A[index] = A[i]; 14 A[i] = min; 15 } 16 }
插入排序:一趟排序不能保证元素到达了最终位置,冒泡、选择一趟就是最终位置。
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5。
1 void insertsort2(int A[], int n) 2 { 3 for (int i = 1; i < n; i++) 4 { 5 int temp = A[i]; 6 7 int j = i - 1; 8 for (; j >= 0; j--) 9 { 10 if (A[j] > temp) A[j + 1] = A[j]; //每次后移一个位置 //写了一个错误语句A[j]>A[[i] 11 /*else break;*/ 12 else { 13 A[j + 1] = temp; break; 14 } 15 } 16 /*A[j + 1] = temp;*/ 17 if (j == -1) A[0] = temp; 18 for (int i = 0; i < n; i++){ 19 cout << A[i] << " "; 20 } 21 cout << endl; 22 23 24 } 25 }
1 void insertsort(int A[], int n) //升序,在有序的序列总寻找插入位置 2 { 3 for (int i = 1; i < n; i++)//序列A[0]~A[i-1]已经有序 4 { 5 int temp = A[i]; 6 for (int j = 0; j < i; j++) //j 范围[0,i-1] 7 { 8 if (A[j] <= A[i]) continue; 9 else //从此时的j到i要开始后移 10 { 11 for (int k = i; k > j; k--) //从前向后寻找插入位置需要写三层循环 12 { 13 A[k] = A[k - 1]; 14 } 15 A[j] = temp; 16 break; 17 18 } 19 20 } 21 } 22 23 }
1 void insertsort3(int A[], int n) 2 { 3 for (int i = 1; i < n; i++) 4 { 5 int temp = A[i]; 6 7 int j = i - 1; 8 while (j >= 0 && A[j]>temp)//此处必须写temp,A[i]会更新 9 { 10 A[j + 1] = A[j]; 11 j--; 12 } 13 /*循环结束时:一、j=-1,即插入位置在0;二、j=i-1;三、插入位置在j+1*/ 14 A[j + 1] = temp; 15 16 } 17 }
快速排序:1、选一个基准元素(一般选择最左边的元素),将序列划分了左右两部分,左边小于基准,右边大于基准
2、对左右子序列重复1的过程,直至序列子序列有序(这对拿来做编程结束点没有帮助)
分析为什么最终会全部有序:序列划分为左右子序列,这两个区间相互是有序的,继续划分区间,所有的区间都有序,直至区间内部也有序,序列整体就有序了。
代码的整体应该是: 此题的难点在于步骤1,如何交换形成相对基准的左右序列;执行语句写在前面,递归调用语句写在后面,先完成一趟划分,在递归进入左右序列进行划分
- 完成一次关于某个基准的左右划分(简单想法是基准两边两个指针,扫描到右边小于基准的,左边大于基准的,就交换元素,但这样行不通,如右边有小的,左边没有大的,这时无法交换)
- quickSort(a, 0,i); quickSort(a,i+1,right) ,此时就是递归深入去划分子区间
1 void quickSort(int a[], int left, int right) 2 { 3 if (left < right) 4 { 5 int i = left; 6 int j = right; 7 int temp = a[i];//每次选最左边元素作为基准 8 while (i != j) 9 { 10 11 //因为在ij移动过程中,有可能出现不满足i!=j的条件 12 while (i < j&&a[j] > temp){ //如果等于的情况怎么办 13 j--; 14 } 15 //可能出现i=j的终止情况 ,用if判断保证循环结束时a[j]<或者=temp 16 if (i < j) { 17 a[i] = a[j]; i++; //i右移一位,原来的a[i]被temp记录了,不会丢失 18 } 19 while (i < j && a[i] < temp) //换个扫描方向 20 { 21 i++; 22 } 23 if (i < j) 24 { 25 a[j] = a[i]; 26 j--; 27 } 28 } 29 //完成了一趟循环,此时i=j的位置为基准的最终位置 30 a[i] = temp; 31 cout << "第" << i <<"个基准"<< a[i] << " "; 32 quickSort(a, 0, i - 1); 33 quickSort(a, i + 1, right); 34 } 35 }
归并排序 :自顶向下的方法,把数组划分了左右数组,一直递归到单个元素序列有序,递归到上层后再合并左右数组。有个关键问题是在对两个有序数组归并的函数中(这两个数组其实是连续的),申请大小为两数组之和的临时数组,存排序结果——不申请额外数组,在原有数组上就变成插入排序了。递归划分的结果是二叉树,深度为lgn,这样每层都会使用n个空间,总共使用nlgn空间,那是否空间复杂度变成了O(nlgn)?这一点暂时不能确定,如果提供一个大小为n的数组,每次都读写该数组,辅助空间就是n。
自底向上的方法是用循环写
参考 http://www.acmerblog.com/merge-sort-5372.html http://blog.csdn.net/yuzhihui_no1/article/details/44223225 (我的代码和这个一样) http://blog.csdn.net/u013074465/article/details/42043967 http://www.cnblogs.com/chengxiao/p/6194356.html
1 #include <algorithm> 2 #include <functional> 3 #include <array> 4 #include <iostream> 5 int temp[100]; //全局的辅助空间 6 void merge(int a[], int l, int mid, int r){ 7 //a[l] -a[mid] a[mid+1]-a[r] 8 int i = l; 9 int j = mid + 1; 10 int k = 0; 11 while ((i <= mid)&&(j <= r)){ 12 if (a[i] <=a[j]) { 13 temp[k++] = a[i++]; 14 } 15 else { 16 temp[k++] = a[j++]; 17 } 18 19 } 20 if (j<=r&&i==mid+1) //这个if语句是可以简化掉的,只用写while循环 21 while (j <= r) temp[k++] = a[j++]; 22 if (i<=mid&& j==r+1) 23 while (i <= mid) temp[k++] = a[i++]; 24 25 for (i = l, k = 0; i <= r; i++,k++){ 26 a[i] = temp[k]; 27 28 } 29 cout << endl; 30 for (int k = l; k < r; k++){ 31 cout << a[k] << " "; 32 } 33 34 cout << endl; 35 } 36 void mergeSort(int a[], int l, int r){ 37 if (l < r){ 38 int mid = (l + r) / 2; 39 mergeSort(a, l, mid); 40 mergeSort(a, mid + 1, r); 41 merge(a, l, mid, r); 42 } 43 } 44 int main(){ 45 int a[] = { 49, 38, 65, 97, 76, 13, 27, 49 }; 46 int c = -1; 47 48 49 mergeSort(a, 0, 7); 50 for (auto x : a){ 51 cout << x << " "; 52 } 53 54 }
对比归并与快排的两种思路,都是递归排序,分解问题的过程是不同的,归并一直分解到元素个数为1,自然有序,然后从底向上合并返回;快排每一次都把序列分为相对有序的两部分,然后继续分解,最终所有子序列有序时整体也就有序了。归并是稳定排序,快排不是,且归并需要消耗额外的空间。

浙公网安备 33010602011771号