一、直接插入排序 基本思想:我们平时玩扑克牌时,摸牌阶段的排序就用到了插入排序的思想
具体实现:
即一般情况下的插入,我们随机列举了一些数字,待插入的数字分为两种情况
我们一开始并不知道数组是不是有序的,所以我们控制下标,end从0开始,将end+1位置的值始终保存到x中,循环进行单趟排序即可,最后结束时end=n-2,n-1位置的数字保存到x中 总体代码: void InsertSort(int* a, int n){ assert(a); for (int i = 0; i < n - 1; ++i) { int end = i; int x=a[end+1];//将end后面的值保存到x里面了 //将x插入到[0,end]的有序区间 while (end >= 0) { if (a[end] > x) { a[end + 1] = a[end]; //往后挪动一位 --end; } else { break; } } a[end + 1] = x; //x放的位置都是end的后一个位置 }} 直接插入排序总结:
最坏的情况下,每次插入一个数字,前面的数字都要挪动一下,一共需要挪动1+2+3+……+n=n(n+1)/2 ③空间复杂度:O(1) 没有借助额外的空间,只用到常数个变量 二、希尔排序基本思想:
例如: 具体实现: ①单组排序 和前面的直接插入相同,就是把原来的间隔为1,现在变为gap了,每组分别进行预排序 ②多组进行排序 ③整个数组进行排序(控制gap) 多次预排序(gap>1)+ 一次插入排序(gap==1) (1)gap越大,预排越快,越不接近于有序 (2)gap越小,预排越慢,越接近有序 结果就是: 总体代码:
希尔排序总结:
三、选择排序基本思想:每次从数组中选出最大的或者最小的,存放在数组的最右边或者最左边,直到全部有序 具体实现: 我们这里进行了优化,一次排序中,直接同时选出最大的数(a[maxi])和最小的数(a[mini])放在最右边和最左边,这样排序效率是原来的2倍
找到最小的数字(a[mini])和最大的数字(a[maxi]),将它们放在最左边和最右边 ps:其中的begin,end保存记录左右的下标,mini,maxi记录保存最小值和最大值的下标
begin++和end--这样下次就可以排剩下的n-2个数字,再次进行单趟,如此可构成循环,直到begin小于end 整体代码: void SelectSort(int* a, int n){ int begin = 0,end = n - 1; while (begin<end) { int mini = begin, maxi = begin; for (int i = begin; i <= end; i++) { if (a[i] < a[mini]) { mini = i; } if (a[i] > a[maxi]) { maxi = i; } } Swap(&a[mini], &a[begin]); //当begin==maxi时,最大值会被换走,修正一下 if (begin==maxi) { maxi=mini; } Swap(&a[maxi], &a[end]); begin++; end--; }} 直接选择排序总结:
四、堆排序基本思想:
具体实现:、
我们将给定的数组序列,建成一个大堆,建堆从根节点开始就需要多次的向下调整算法
向下调整算法的基本思想:
建堆图示: ![]() //最后一个叶子结点的父亲为i,从后往前,依次向下调整,直到调到根的位置 for (int i = (n - 1 - 1) / 2;i>=0;--i) { AdJustDown(a,n,i); }
![]()
整体代码如下: void AdJustDown(int* a, int n, int parent) { int child = parent * 2 + 1; while (child < n) { if (child + 1 < n && a[child+1] > a[child ]) { child++; } if(a[child]>a[parent]) { Swap(&a[child], &a[parent]); parent = child; child = parent * 2 + 1; } else { break; } } } //堆排序 void HeapSort(int*a,int n) { for (int i = (n - 1 - 1) / 2;i>=0;--i) { AdJustDown(a,n,i); } for (int end = n - 1; end > 0; --end) { Swap(&a[end],&a[0]); AdJustDown(a,end,0); } } 五、冒泡排序![]() 冒泡排序的基本思想:一趟过程中,前后两个数依次比较,将较大的数字往后推,下一次只需要比较剩下的n-1个数,如此往复
冒泡排序总结:
六、快速排序![]() 递归版本 1、hoare版本
动图演示: ![]() //hoare版本 //单趟排序 让key到正确的位置上 keyi表示key的下标,并不是该位置的值 int partion1(int* a, int left, int right) { int keyi = left;//左边作keyi while (left < right) { //右边先走,找小于keyi的值 while (left < right && a[right] >= a[keyi]) { right--; } //左边后走,找大于keyi的值 while (left < right && a[left] <= a[keyi]) { left++; } Swap(&a[left], &a[right]); } Swap(&a[left], &a[keyi]); return left; } void QuickSort(int* a, int left, int right) { if (left >= right) return; int keyi = partion1(a, left, right); //[left,keyi-1] keyi [keyi+1,right] QuickSort(a, left, keyi - 1); QuickSort(a, keyi + 1, right); } 2、挖坑法其实本质上是hoare的变形
动图演示: ![]()
3、前后指针法(推荐这种写法)前后指针的思想:
这样key同样到达了正确的位置 动图演示: ![]() int partion3(int* a, int left, int right) 递归展开图 ![]() 快速排序的优化1、三数取中法 快速排序对于数据是敏感的,如果这个序列是非常无序,杂乱无章的,那么快速排序的效率是非常高的,可是如果数列有序,时间复杂度就会从O(N*logN)变为O(N^2),相当于冒泡排序了 若每趟排序所选的key都正好是该序列的中间值,即单趟排序结束后key位于序列正中间,那么快速排序的时间复杂度就是O(NlogN) 但是这是理想情况,当我们面对一组极端情况下的序列,就是有序的数组,选择左边作为key值的话,那么就会退化为O(N^2)的复杂度,所以此时我们选择首位置,尾位置,中间位置的数分别作为三数,选出中间位置的数,放到最左边,这样选key还是从左边开始,这样优化后,全部都变成了理想情况 ![]()
2、递归到小子区间随着递归深度的增加,递归次数以每层2倍的速度增加,这对效率有着很大的影响,当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排 我们可以当划分区间长度小于10的时候,用插入排序对剩下的数进行排序 //小区间优化法,可以采用直接插入排序 void QuickSort(int* a, int left, int right) { if (left >= right) return; if (right - left + 1 < 10) { InsertSort(a + left, right - left + 1); } else { int keyi = partion5(a, left, right); //[left,keyi-1] keyi [keyi+1,right] QuickSort(a, left, keyi - 1); QuickSort(a, keyi + 1, right); } } 非递归版本递归的算法主要是在划分子区间,如果要非递归实现快排,只要使用一个栈来保存区间就可以了。一般将递归程序改成非递归首先想到的就是使用栈,因为递归本身就是一个压栈的过程。
快速排序的总结:
七、归并排序![]() 归并排序的基本思想(分治思想):
具体实现:
递归实现:从思想上来说和二叉树很相似,所以我们可以用递归的方法来实现归并排序 代码如下: void _MergeSort(int* a, int left, int right, int* tmp) { if (left >= right) { return; } int mid = (left + right) / 2; _MergeSort(a, left, mid, tmp); _MergeSort(a, mid+1, right, tmp); int begin1 = left, end1 = mid; int begin2 = mid + 1, end2 = right; int i = left; while (begin1 <= end1 && begin2 <= end2) { if (a[begin1] < a[begin2]) { tmp[i++] = a[begin1++]; } else { tmp[i++] = a[begin2++]; } } while (begin1 <= end1) { tmp[i++] = a[begin1++]; } while (begin2 <= end2) { tmp[i++] = a[begin2++]; } for (int j = left; j <= right; j++) { a[j] = tmp[j]; } } //归并排序 void MergeSort(int* a, int n) { int* tmp = (int*)malloc(sizeof(int)*n); if (tmp == NULL) { printf('malloc fail\n'); exit(-1); } _MergeSort(a,0,n-1,tmp); free(tmp); tmp = NULL; } 非递归实现:我们知道,递归实现的缺点就是会一直调用栈,而栈内存往往是很小的。所以,我们尝试着用循环的办法去实现 由于我们操纵的是数组的下标,所以我们需要借助数组,来帮我们存储上面递归得到的数组下标,和递归的区别就是,递归要将区间一直细分,要将左区间一直递归划分完了,再递归划分右区间,而借助数组的非递归是一次性就将数据处理完毕,并且每次都将下标拷贝回原数组 归并排序的基本思路是将待排序序列a[0…n-1]看成是n个长度为1的有序序列,将相邻的有序表成对归并,得到n/2个长度为2的有序表;将这些有序序列再次归并,得到n/4个长度为4的有序序列;如此反复进行下去,最后得到一个长度为n的有序序列。 ![]() 但是我们这是理想情况下(偶数个),还有特殊的边界控制,当数据个数不是偶数个时,我们所分的gap组,势必会有越界的地方 第一种情况: ![]() 第二种情况: ![]() 代码如下:
归并排序的总结:
八、计数排序又叫非比较排序,又称为鸽巢原理,是对哈希直接定址法的变形应用 基本思想:
具体实现:
对于给定的任意数组a,我们需要开辟一个计数数组count,a[i]是几,就对count数组下标是几++ ![]() 这里我们用到了绝对映射,即a[i]中的数组元素是几,我们就在count数组下标是几的位置++,但是对于数据比较聚集,不是从较小的数字开始,例如1001,1002,1003,1004这样的数据,我们就可以用到相对映射的方法,以免开辟数组空间的浪费,count数组的空间大小我们可以用a数组中最大值减去最小值+1来确定(即:range=max-min+1),我们可以得到count数组下标 j =a[i]-min ![]()
count[j]中数据是几,说明该数出现了几次,是0就不用拷贝 ![]() 代码如下: void CountSort(int* a, int n) { int min = a[0], max = a[0];//如果不赋值,min和max就是默认随机值,最好给赋值一个a[0] for (int i=1;i<n;i++)//修正 找出A数组中的最大值和最小值 { if (a[i] < min) { min=a[i]; } if (a[i]>max) { max=a[i]; } } int range = max - min + 1;//控制新开数组的大小,以免空间浪费 int* count = (int*)malloc(sizeof(int) * range); memset(count,0, sizeof(int) * range);//初始化为全0 if (count==NULL) { printf('malloc fail\n'); exit(-1); } //1、统计数据个数 for (int i=0;i<n;i++) { count[a[i]-min]++; } //2、拷贝回A数组 int j = 0; for (int i=0;i<range;i++) { while (count[i]--) { a[j++] = i + min; } } free(count); count = NULL; } 计数排序总结:
八大排序的稳定性总结:稳定的排序有:直接插入排序、冒泡排序、归并排序 不稳定的排序有:希尔排序、选择排序、堆排序、快速排序、计数排序 |
|
来自: 菌心说 > 《编程+、计算机、信息技术》