目录
插入排序
插入排序
希尔排序
选择排序
选择排序
堆排序
交换排序
冒泡排序
快速排序
递归实现:
●hoare版本
●三数取中+小区间法优化
●挖坑版本
●双指针版本
非递归
●用栈实现
●用队列实现
归并排序
● 递归
●非递归
总结
来了朋友,等你很久了,是不是很孤独,没人陪你聊天。快一起学习!!!
前言
排序的运用场景非常广泛,在我们的日常生活中经常出现,例如:根据销量排名选择店家购物,期末成绩排名,全国高校排名等等。如下图:不同的排序方法都有自己的特点,接下来就一起感受排序的魅力吧!
插入排序
插入排序
排序思想:把待排序的值,插入到一个已经排好序的有序序列中,得到一个新的有序序列。简单的理解就是,假设在你面前有一个身高从矮到高的队列,现在要将你插入到队列当中,插入后保持队列仍然是从小到大。
思路分析:
还是以站队为例,原队列中有n个人,那么我就是第n+1个。我和队尾的个子最高的比较一下身高,如果我比他高,那么直接排在队尾。反之,他来到n+1的位置,我继续和前面的同学进行比较,直至我不在低于前面的同学。一次插入就完成了!
关于边界问题,视频中最后库里和哈登比较完后,end的位置应该更新到下标为-1的位置,end+1也就是0的位置,最后前面没有人可以比较,库里插入到end+1的位置。视频中没有体现出来,希望不要被误导。
代码分析:
定义一个变量tmp记录end+1位置(新插入的数据)的值,如果tmp的值小于end位置的值,那么end的值就向后挪动,并且更新end的位置,下次tmp要继续和前面的值比较。循环结束的条件是end>=0,如果tmp找到了位置,直接跳出循环。如图:一次插入的过程
代码实现:
//插入排序 void InsertSort(int* arr, int sz) { for (int i = 0; i < sz - 1; i++) { int end = i; int tmp = arr[end + 1]; while (end>=0) { if (tmp < arr[end]) { arr[end + 1] = arr[end]; end--; } else { break; } } arr[end + 1] = tmp; } }
希尔排序
排序思想:
先选定一个整数(gap),把待排序数据分成gap组,所有距离为gap的数据分在同一组内,并对每一组内进行排序。gap按照特定的规则减少,重复上述分组和排序的工作。当到达gap=1时,完成排序。
希尔排序是对插入排序的优化。插入排序的长处是处理接近有序的数据,所以在插入排序的基础上用gap将数据分组处理。当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,也就是直接插入排序,这时数组已经接近有序了。
代码实现:
//希尔排序 void HellSort(int* arr, int sz, int gad) { while (gad > 1) { //gad > 1 预排序 //gad = 1, 插入排序 gad = gad / 3 + 1; for (int i = 0; i < sz - gad; i++) { int end = i; int tmp = arr[end + gad]; while (end >= 0) { if (tmp < arr[end]) { arr[end + gad] = arr[end]; end -= gad; } else { break; } } arr[end + gad] = tmp; } } }
选择排序
选择排序
排序思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置(或者末尾),直到所有数据排完 。在排序思想的基础上做下优化,一次遍历找到数据中最大和最小的两个值。
第一趟排序找到最大的和最小的,mini = 2,maxi = 4;大的值交换值队尾,小的值交换值队首。下次排序的区间减小。
第二趟找到次大的和次小的,mini = 3,maxi = 1;大的值交换值队尾,小的值交换值队首。下次排序的区间减小。
第三趟,mini = 4,maxi =2。大的值交换值队尾,小的值交换值队首。
需要注意的就是,找到大小两个数据的下标后,maxi 可能和 begin相等,正如上图中的第二趟,最高的是科比,maxi和begin相等,这种情况需要特殊处理一下。
这种情况下,第一次begig和mini的值交换后arr[begin] = 0,arr[mini] = 9。这时arr[end]和arr[maxi]交换的时候,又把两个值给换回去了。所以要判断下这种情况,maxi 等于 begin的时候 maxi的值改为mini的值。(处理方法不唯一,不要忘记这种情况就好)。
代码实现:
交换后面也会用到,这里封装成一个函数。
//交换 void Swap(int* e1, int* e2) { int tmp = *e1; *e1 = *e2; *e2 = tmp; }
//选择排序 void SelectSort(int* arr, int sz) { int begin = 0; int end = sz - 1; while (begin < end) { int mini = begin; int maxi = begin; for (int i = begin+1; i <= end; i++) { if (arr[i] < arr[mini]) { mini = i; } if (arr[i] > arr[maxi]) { maxi = i; } } //交换 Swap(&arr[begin],&arr[mini]); if (maxi == begin) { maxi = mini; } Swap(&arr[end],&arr[maxi]); end--; begin++; } }
堆排序
堆排序在上篇文章中介绍过了,这里不在重复。
建堆的过程注意升序建大堆,降序建小堆。
代码实现:
//向下调整 void AdjustDown(int* arr,int parent,int sz) { int chlid = parent * 2 + 1; while (chlid < sz) { if (chlid + 1 < sz && arr[chlid+1]> arr[chlid]) { chlid = parent * 2 + 2; } if (arr[chlid] > arr[parent]) { //交换 Swap(&arr[chlid],&arr[parent]); //迭代 parent = chlid; chlid = parent * 2 + 1; } else { break; } } } //建堆算法 void HeapCreate(int* arr, int sz) { for (int i = ((sz - 1) - 1) / 2; i >= 0; i--) { AdjustDown(arr,i,sz); } } //堆排序 void HeapSort(int* arr, int sz) { //建堆 HeapCreate(arr,sz); //PrintSort(arr, sz); //升序 int end = sz - 1; while (end) { //交换 Swap(&arr[0],&arr[end]); //向下调整 AdjustDown(arr,0,end); end--; } }
交换排序
冒泡排序
排序思想:根据序列中相邻两个值的比较结果来对换这两个值在序列中的位置。将较大的向序列的尾部移动,将较小的向序列的前部移动。(升序)
代码实现:
//冒泡排序 void BubbleSort(int* arr, int sz) { //总趟数 for (int i = 0; i < sz - 1; i++) { //一趟 for (int j = 0; j < sz - 1 - i; j++) { if (arr[j] > arr[j + 1]) { //交换 Swap(&arr[j],&arr[j+1]); } } } }
快速排序
1960年Tony Hoare发布了快速排序算法(Quick Sort),这个算法也是当前世界上使用最广泛的算法之一。
排序思想:
选取一个基准值,将待排序集合分割成两个子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,这就划分成了子问题,左右子序列重复该过程,直到所有元素都排列在相应位置上。
递归实现:
●hoare版本
分析:取begin位置的值为基准值,left和right记录第一个数据和最后一个数据的位置。假设right先向左走,遇到小于基准值的数据停下来。接着left向右移动,找到比基准值大的数据停下来。left和right位置的值交换一下,重复该过程,直至left和right的位置相遇。它们相遇的位置一定小于基准值。如果是right停止(right找到的值是比基准值小的),left停止,left停下来后要和right位置交换值,此时left位置的值也小于基准值。画图分析:
初始状态
代码实现:
//快速排序 void QuickSort(int* arr, int begin, int end) { if (begin >= end) { return; } int left = begin; int right = end; int key = left; while (left < right) { //右边先走,找小 while (left
= arr[key]) { right--; } while(left < right && arr[right] <= arr[key]) { left++; } Swap(&arr[left],&arr[right]); } Swap(&arr[left],&arr[key]); key = left; QuickSort(arr,begin,key-1); QuickSort(arr,key+1,end); } ●三数取中+小区间法优化
三数取中:这项优化主要是针对基准值的选择,当基准值在整个数据中偏大或者偏小都会降低快排的性能,所以在选择基准值的时候,在三个数据中选择中间大的一个作为基准值,在一定程度上达到了优化
//三数取中 int GetMiddle(int* arr,int begin, int end) { int mid = (begin + end) / 2; if (arr[begin] > arr[mid] ) { if (arr[mid] >arr [end]) { return mid; } else if (arr[end] > arr[begin]) { return begin; } else { return end; } } else { if (arr[mid] < arr[end]) { return mid; } else if (arr[end] < arr[begin]) { return begin; } else { return end; } } }
小区间法优化:
这项优化是考虑到当数据的数量较少的时候,继续递归快排效率不是很高,所以当区间较小的时候,可以考虑用插入排序直接解决。
优化后的快排:
优化后 void QuickSort(int* arr, int begin, int end) { if (begin >= end) { return; } //小区间优化 if ((end - begin + 1) < 15) { InsertSort(arr,end-begin+1); } else { //三数取中优化 int mid = CetMiddle(arr, begin, end); Swap(&arr[begin], &arr[mid]); int left = begin; int right = end; int key = left; while (left < right) { //右边先走,找小 while (left < right && arr[right] >= arr[key]) { right--; } while (left < right && arr[left] <= arr[key]) { left++; } Swap(&arr[left], &arr[right]); } Swap(&arr[left], &arr[key]); key = left; QuickSort(arr, begin, key - 1); QuickSort(arr, key + 1, end); } }
●挖坑版本
思路:保存基准值,基准值的位置变成坑位。假设right先向左走,找到小于基准值的数停下来,将right位置的值填入坑位,right位置变成新坑位。left向右找到大于基准值的数据停下来,left位置的值填入坑位,left位置变成新坑位。left和right相遇的地方填入基准值,坑位左边的小于基准值,右边大于基准值。
画图分析:
代码实现:
//挖坑法 void HoleSort(int* arr, int begin, int end) { if (begin >= end) { return; } int left = begin; int right = end; int key = arr[left]; //坑位 int hole = left; while (left < right) { //右边先走,找小 while (left < right && arr[right] >= key) { right--; } Swap(&arr[hole],&arr[right]); //新坑位 hole = right; //左边找大 while (left < right && arr[left] <= key) { left++; } Swap(&arr[hole],&arr[left]); //新坑位 hole = left; } //坑位的位置 arr[hole] = key; //[begin,hore-1]hore[hore+1,end] HoleSort(arr, begin,hole-1); HoleSort(arr,hole+1,end); }
●双指针版本
思路:key记录基准值的位置,prev和cur一前一后。当cur的位置越界时,一趟排序结束。cur从左边开始向右走,当cur位置的值比基准值小的时候,和++prev位置的值交换,这样是保证prev位置和以前位置的值都是小于基准值的。重复此过程直至cur遍历完所有元素,这时prev位置的值和基准值交换。这样prev以前的值都是小于基准值的。
画图分析:
代码实现:
void DoublePointer(int* arr, int begin, int end) { if (begin >= end) { return; } int cur = begin + 1; int prev = begin; int key = begin; while (cur <= end) { //cur的值比key小,保证prev之前的值全部是比基准值小的 if (arr[cur] < arr[key] && ++prev != cur) { Swap(&arr[prev],&arr[cur]); } cur++; } //prev位置的值是小于key的 Swap(&arr[prev],&arr[key]); //新区间【begin ,prev-1】【prev+1,end】 DoublePointer(arr,begin,prev-1); DoublePointer(arr,prev+1,end); }
非递归
用栈或者队列实现快排。将区间入栈,或者队列。通过获取栈顶或者堆顶得到要排序的区间,出栈,并完成一次排序。根据key的新位置,将两段新的区间入栈。
●用栈实现
栈的部分接口:
typedef int StackDateType; typedef struct StackNode { StackDateType* arr; int size; int capacity; }St; void StackInit(St* st) { assert(st); st->arr = NULL; st->capacity = 0; st->size = 0; } void StackDestory(St* st) { assert(st); st->arr = NULL; st->capacity = st->size = 0; } void StackPush(St* st, StackDateType data) { //扩容 if (st->capacity == st->size) { int NewCapcity = st->capacity == 0 ? 4 : st->capacity * 2; StackDateType* tmp = (StackDateType*)realloc(st->arr,NewCapcity*sizeof(StackDateType)); if (tmp == NULL) { perror("malloc faile:"); exit(-1); } st->arr = tmp; st->capacity = NewCapcity; } st->arr[st->size] = data; st->size++; } void StackPop(St* st) { assert(st); assert(st->size > 0); st->size--; } StackDateType StackTop(St* st) { assert(st); return st->arr[st->size-1]; } bool StackEmpty(St* st) { assert(st); return st->size == 0; }
排序逻辑代码:
int PartSort3(int* arr, int begin, int end) { int mid = GetMiddle(arr, begin, end); Swap(&arr[begin], &arr[mid]); int prev = begin; int cur = begin + 1; int key = begin; while (cur <= end) { // 找到比key小的值时,跟++prev位置交换,小的往前翻,大的往后翻 if (arr[cur] < arr[key] && ++prev != cur) { Swap(&arr[cur],&arr[prev]); } cur++; } //prev的值一定比key要小 Swap(&arr[prev], &arr[key]); key = prev; return key; }
//非递归用栈 void QuickSortNorR1(int* arr, int begin, int end) { St st; StackInit(&st); StackPush(&st,begin); StackPush(&st,end); while (!StackEmpty(&st)) { int right = StackTop(&st); StackPop(&st); int left = StackTop(&st); StackPop(&st); //一趟排序 int key = PartSort3(arr, left, right); //[begin ,key-1] key [key+1,end] //新的区间入栈 if (key +1 < right) { StackPush(&st, key + 1); StackPush(&st, right); } if (left < key - 1) { StackPush(&st,left); StackPush(&st,key-1); } } StackDestory(&st); }
●用队列实现
//非递归用队列 void QuickSortNorR2(int* arr, int begin, int end) { int Que[20] = {0}; int head = begin; int tail = begin; //先进先出 Que[tail++] = begin; Que[tail++] = end; while (head < tail) { //出队获取左右区间 int left = Que[head++]; int right = Que[head++]; //一趟排序 int key = PartSort3(arr, left, right); //[begin ,key-1] key [key+1,end] if (left < key - 1) { //入队 Que[tail++] = left; Que[tail++] = key - 1; } if (key + 1 < right) { Que[tail++] = key+1; Que[tail++] = right; } } }
归并排序
排序思想:使每个子序列有序,再使子序列段间有序。
● 递归
分析:归并的过程是将两组有序的序列归并成一组有序的序列,但是在实际问题中,我们要处理的数据大都是随机的,所以当将子序列分到最小,一组只有一个数据的时候,这时候就认为这个子序列是有序的。从一个数据开始,两两归并。归并成一组有两个元素的有序序列,在继续归并,直至序列有序。
代码实现:
void _MergeSort(int* arr, int* tmp, int begin, int end) { if (begin >= end) { return; } int mid = (begin + end) / 2; //[begin,mid] [mid+1,end] _MergeSort(arr,tmp,begin,mid); _MergeSort(arr,tmp,mid+1,end); int begin1 = begin; int end1 = mid; int begin2 = mid + 1; int end2 = end; int i = begin; while (begin1 <= end1 && begin2 <= end2) { if (arr[begin1] > arr[begin2]) { tmp[i++] = arr[begin2++]; } else { tmp[i++] = arr[begin1++]; } } while (begin1 <= end1) { tmp[i++] = arr[begin1++]; } while (begin2 <= end2) { tmp[i++] = arr[begin2++]; } //拷贝 memcpy(arr+begin,tmp+begin,sizeof(int)*(end-begin+1)); } //归并排序,递归 void MergeSort(int* arr, int len) { int* tmp = (int*)calloc(len,sizeof(int)); if (tmp == NULL) { perror("malloc faile:"); exit(-1); } _MergeSort(arr,tmp,0,len-1); free(tmp); tmp = NULL; }
●非递归
越界的三种情况:
gap表示一组有几个元素。这里的边界需要注意一下,i += 2*gap的原因,就是跳过两组取处理后面的数据。最后gap*=2是在下次处理的时候,每组的元素个数翻倍。
代码实现:
void MergeSortNorR(int* arr, int len) { int* tmp = (int*)calloc(len, sizeof(int)); if (tmp == NULL) { perror("malloc fail:"); exit(-1); } int gap = 1; while (gap < len) { for (int i = 0; i < len; i += 2 * gap) { int begin1 = i, end1 = i + gap - 1; int begin2 = i + gap,end2 = i+2*gap-1; int index = i; if (end1 >= len) { end1 = len - 1; //修正成不存在区间 begin2 = len; end2 = len - 1; } else if (begin2 >= len) { begin2 = len; end2 = len - 1; } else if (end2 >= len) { end2 = len - 1; } while(begin1 <= end1 && begin2 <= end2) { if (arr[begin1] <= arr[begin2]) { tmp[index++] = arr[begin1++]; } else { tmp[index++] = arr[begin2++]; } } while (begin1 <= end1) { tmp[index++] = arr[begin1++]; } while (begin2 <= end2) { tmp[index++] = arr[begin2++]; } } memcpy(arr, tmp, sizeof(int)*len); gap *= 2; } free(tmp); tmp = NULL; }
总结
时间复杂度和空间复杂度大家都非常了解,稳定性指定是在排序的过程中相同的值在排序后是否可以保证相对顺序,如果可以则视该排序稳定,反之不稳定。