排序
排序:排序就是使一串记录按照其中某个或者某些关键字的大小,递增或者递减排序起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
常见的排序方法
排序的测试对比代码
// 测试排序的性能对比 void TestOP() { srand(time(0)); const int N = 100000; int* a1 = (int*)malloc(sizeof(int) * N); int* a2 = (int*)malloc(sizeof(int) * N); int* a3 = (int*)malloc(sizeof(int) * N); int* a4 = (int*)malloc(sizeof(int) * N); int* a5 = (int*)malloc(sizeof(int) * N); int* a6 = (int*)malloc(sizeof(int) * N); for (int i = 0; i < N; ++i) { a1[i] = rand(); a2[i] = a1[i]; a3[i] = a1[i]; a4[i] = a1[i]; a5[i] = a1[i]; a6[i] = a1[i]; } int begin1 = clock(); InsertSort(a1, N); int end1 = clock(); int begin2 = clock(); ShellSort(a2, N); int end2 = clock(); int begin3 = clock(); SelectSort(a3, N); int end3 = clock(); int begin4 = clock(); HeapSort(a4, N); int end4 = clock(); int begin5 = clock(); QuickSort(a5, 0, N - 1); int end5 = clock(); int begin6 = clock(); MergeSort(a6, N); int end6 = clock(); printf("InsertSort:%d\n", end1 - begin1); printf("ShellSort:%d\n", end2 - begin2); printf("SelectSort:%d\n", end3 - begin3); printf("HeapSort:%d\n", end4 - begin4); printf("QuickSort:%d\n", end5 - begin5); printf("MergeSort:%d\n", end6 - begin6); free(a1); free(a2); free(a3); free(a4); free(a5); free(a6); }
常见排序算法的实现
插入排序
直接插入排序
插入排序,是类似在玩牌或麻将之类需要接收一个元素,然后将接收到的元素放置合理的排序外置。
官方解释为:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
直接插入排序实现
思路:假设升序,从第一个开始一个一个遍历数组,第i个大于第i+1个,向后移动一个,再与前面比较,循环往复…即可实现
//直接插入排序 void InsertSort(int* arr, int n) { for (int pos = 1; pos < n; pos++ ) { int tmp = arr[pos]; int end = pos; while (end > 0) { if (tmp < arr[end-1]) { arr[end] = arr[end - 1]; } else { break; } end--; } arr[end] = tmp; } }
直接插入排序的特点
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:最好O(N),最坏O(N2)
- 空间复杂度:O(1)
- 稳定性:稳定
希尔排序(缩小增量排序)
希尔排序又称缩小增量排序,基本思想为先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
简单思路:将一个较长的数组,通过等差的距离取值形成一个数组,通过将每个等差距离的数组进行插入排序,将整体数组接近有序,然后通过将等差的距离减小,可以更加准确的接近有序,直到等差距离为1,数组排序成功。
希尔排序实现
思路:
1.预排序——目标数组接近有序(分组插排)
2.直接插入排序
//希尔排序 void ShellSort(int* arr, int n) { int gap = n; while (gap >= 1) { gap /= 2; for (int i = 0; i < gap; i++) { for (int pos = i; pos < n - gap; pos += gap) { int end = pos; int tmp = arr[pos + gap]; while (end >= 0) { if (tmp < arr[end]) { arr[end + gap] = arr[end]; } else { break; } end -= gap; } arr[end + gap] = tmp; } } } }
//希尔排序 void ShellSort(int* arr, int n) { int gap = n; while (gap >= 1) { gap /= 2; for (int pos = 0; pos < n-gap; pos ++) { int end = pos; int tmp = arr[pos + gap]; while (end >= 0) { if (tmp < arr[end]) { arr[end + gap] = arr[end]; } else { break; } end -= gap; } arr[end + gap] = tmp; } } }
希尔排序的特点
- 间隔gap分为一组,对每组数据插入排序,gap越大,排序越快,越不接近有序;反之,gap越小,排序越慢,越接近有序。
- gap>1,预排序;gap=1,直接插入排序
- 希尔排序是直接插入排序的优化
- 希尔排序的时间复杂度大约为O(N1.3)
选择排序
直接选择排序
直接选择排序类似于将一堆未清洗的牌中选出一个最小(大)的牌放在起始位置,一次排序。
简单思路为:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
直接选择排序的实现
步骤:
- 在数组元素中选出最小(大)的数据元素
- 将其交换至起始位置
- 缩小范围,循环操作
//直接选择排序 void SelectSort(int* arr, int n) { int left = 0; int right = n - 1; //循环缩小排序范围 while (left<right) { int mini = left; int maxi = left; //找最小,最大 int i = 0; for (i = left; i <= right; i++) { if (arr[mini] >= arr[i]) { mini = i; } if (arr[maxi] <= arr[i]) { maxi = i; } } //交换 Swap(&arr[left], &arr[mini]); if (left == maxi) { maxi = mini; } Swap(&arr[right], &arr[maxi]); left++; right--; } }
直接选择排序的特点
- 选择排序的时间复杂度最好为O(N2),最坏也为O(N2)
- 效率不高,不经常使用
堆排序
链接: 【数据结构】堆
堆排序的特点
- 堆排序的时间复杂度O(NlogN)
- 效率较高
交换排序
冒泡排序
冒泡排序是比较简单且容易上手的排序,其简单思路:从第一个元素开始,比较其与其下一个元素的大小,以升序为例,若下一个元素小于前者,则交换;元素位置移动到下一个位置,一次循环可以将最大值移动到最后,多次循环即可达成升序效果。
//冒泡排序 #include<stdio.h> void Swap(int* x, int* y) { int tmp = *x; *x = *y; *y = tmp; } void BubbleSort(int* arr, int sz) { for (int j = sz; j > 0; j--) { for (int i = 0; i < j; i++) { if (arr[i] < arr[i + 1]) { Swap(&arr[i], &arr[i + 1]); } } } } int main(void) { int arr[] = { 2,4,6,8,0,1,3,5,7,9 }; int sz = sizeof(arr) / sizeof(arr[0]); BubbleSort(arr,sz); for (int i = 0; i < sz; i++) { printf("%d ", arr[i]); } return 0; }
冒泡排序的特点
- 时间复杂度最坏为O(N2),最好为O(N2)/O(N)
比较直接插入排序与冒泡排序
相同的时间复杂度代表着处于相同的量级,但是处于不同的量级也会有不同的差异。
- 有序:一样
- 接近有序:有一些差距
- 部分有序:差距很大
总体来讲:直接插入排序在N2量级时排序较快
快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法。
基本思想:任取元素数据中的某值为基准值,将该排序码分割成俩部分,左方均小于(大于)基准值,右方均大于(小于)基准值,然后左右部分再次挑选基准值进行循坏分割。
基本框架:
1.选择一种方法将比基准值小的放左边,基准值大的放右边
2.利用递归,将原基准值左边的值继续进行快速排序,右边也如此。
//快速排序 void QuickSort(int* arr, int left, int right) { if (left >= right) return; //hoare法排序 int key = HoareSort(arr, left, right); //挖坑法排序 int key = HoleSort(arr, left, right); //前后指针法排序 int key = PointSort(arr, left, right); QuickSort(arr, left, key); QuickSort(arr, key + 1, right); }
hoare法
hoare法的基本思路:
1.R(right)先出发,寻找比基准值(key)小的值
2.L(left)后出发,寻找比基准值(key)大的值
3.交换(swap)
4.若相遇,将该位置的值与基准值交换
//hoare法 int HoareSort(int* arr, int left, int right) { 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[key], &arr[left]); key = left; return key; }
问题:为何相遇位置一定比keyi小?
左边做key,右边先走,保证相遇位置的值比key小。
原因时:相遇存在俩种情况:
1.R先走,R找到小,L找大时没有找到,反而找到了R,此时R找到值为小,即相遇位置的值比key小;
2.R先走,R找小没有找到,R直接找到L,此时L为已经交换的小值,即相遇位置的值比key小。
挖坑法
挖坑法的基本思路:
1.先将关键值(key)保存,将其位置设置成一个坑位(hole)
2.R(right)先出发,寻找比基准值(key)小的值,将其保存在坑位,其位置设置成新坑位
3.L(left)后出发,寻找比基准值(key)大的值,将其保存在坑位,其位置设置成新坑位
4.相遇后将关键值(key)保存在坑位即可
//挖坑法排序 int HoleSort(int* arr, int left, int right) { int key = arr[left]; int hole = left; while(left<right) { while (left < right && arr[right] >= key) { right--; } arr[hole] = arr[right]; hole = right; while (left < right && arr[left] <= key) { left++; } arr[hole] = arr[left]; hole = left; } arr[hole] = key; key = left; return key; }
前后指针法
前后指针法的基本思路:
1.cur与prev都从左边开始出发
2.cur先出发寻找比key小的值,找到后++prev,cur和prev位置的值进行交换
3.cur找到比key大的值,++cur
【说明】
1.prev要么紧跟cur(prev下一个就是cur)
2.prev要么跟着cur中间间隔着比key大的一段值区间
//前后指针法排序 int PointSort(int* arr, int left, int right) { int key = left; int cur = left; int prev = left; while (cur <= right) { if ( arr[cur] < arr[key]&& ++prev != cur) { Swap(&arr[cur], &arr[prev]); } cur++; } Swap(&arr[key], &arr[prev]); key = prev; return key; }
三种代码的整体实现
//交换 void Swap(int* x, int* y) { int tmp = *x; *x = *y; *y = tmp; } //三数选中 int GetMiddle(int arr[], int left, int right) { int midi = (left + right) / 2; if (arr[left] > arr[midi]) { if (arr[midi] > arr[right]) return midi; else if (arr[left] > arr[right]) return right; else return left; } else { if (arr[left] > arr[right]) return left; else if (arr[midi] > arr[right]) return right; else return midi; } } //快速排序 void QuickSort(int* arr, int left, int right) { if (left >= right) return; //hoare法排序 int key = HoareSort(arr, left, right); //挖坑法排序 //int key = HoleSort(arr, left, right); //前后指针法排序 //int key = PointSort(arr, left, right); QuickSort(arr, left, key); QuickSort(arr, key + 1, right); } //hoare法 int HoareSort(int* arr, int left, int right) { //随机选key /*int midi = left + rand() % (right - left); Swap(&arr[left], &arr[midi]);*/ //三数选中 int midi = GetMiddle(arr,left, right); if (midi != left) { Swap(&arr[left], &arr[midi]); } 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[key], &arr[left]); key = left; return key; } //挖坑法排序 int HoleSort(int* arr, int left, int right) { //随机选key /*int midi = left + rand() % (right - left); Swap(&arr[left], &arr[midi]);*/ //三数选中 int midi = GetMiddle(arr, left, right); if (midi != left) { Swap(&arr[left], &arr[midi]); } int key = arr[left]; int hole = left; while(left<right) { while (left < right && arr[right] >= key) { right--; } arr[hole] = arr[right]; hole = right; while (left < right && arr[left] <= key) { left++; } arr[hole] = arr[left]; hole = left; } arr[hole] = key; key = left; return key; } //前后指针法排序 int PointSort(int* arr, int left, int right) { //随机选key /*int midi = left + rand() % (right - left); Swap(&arr[left], &arr[midi]);*/ //三数选中 int midi = GetMiddle(arr, left, right); if (midi != left) { Swap(&arr[left], &arr[midi]); } int key = left; int cur = left; int prev = left; while (cur <= right) { if ( arr[cur] < arr[key]&& ++prev != cur) { Swap(&arr[cur], &arr[prev]); } cur++; } Swap(&arr[key], &arr[prev]); key = prev; return key; }
快速排序的优化
首先我们应该了解快速排序的时间复杂度为O(NlogN),但如果要排序的元素已经是升序或者降序时,其实际的实际复杂度为O(N2),这种情况有可能会导致栈溢出。
说明key越接近中间,效率越高,二分效率也会更高
方法1:随机选key
//随机选key int midi = left + rand() % (right - left); Swap(&arr[left], &arr[midi]);
方法2:三数选中
//三数选中 int GetMiddle(int arr[], int left, int right) { int midi = (left + right) / 2; if (arr[left] > arr[midi]) { if (arr[midi] > arr[right]) return midi; else if (arr[left] > arr[right]) return right; else return left; } else { if (arr[left] > arr[right]) return left; else if (arr[midi] > arr[right]) return right; else return midi; } }
//三数选中 int midi = GetMiddle(arr,left, right); if (midi != left) { Swap(&arr[left], &arr[midi]); }
方法3:小区间优化
原因:当递归次数非常多时,最后一次递归的次数占据二分之一,可以进行小区间优化,当区间小于10时,额可以进行直接插入排序。
- 相比较冒泡排序,直接插入排序存在部分升序,可以增大效率,而冒泡排序则不会考虑这种情况
//快速排序 void QuickSort(int* arr, int left, int right) { if (left >= right) return; if (right - left <= 10) { //直接插入排序 InsertSort(arr + left, right - left + 1); return; } else { //hoare法排序 //int key = HoareSort(arr, left, right); //挖坑法排序 //int key = HoleSort(arr, left, right); //前后指针法排序 int key = PointSort(arr, left, right); QuickSort(arr, left, key); QuickSort(arr, key + 1, right); } }
快速排序的特点
- 时间复杂度:O(NlogN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
快速排序非递归
快速排序在使用递归时会出现的问题:
1.效率问题
2.在使用递归时,递归太深会导致栈溢出(stack overflow),递归会建立栈帧,当建立的栈帧太多时会导致移溢出。
方法:(递归变非递归)
1.直接改循环(斐波那契)
2.使用栈辅助改循环
快速排序递归变非递归时可以使用使用栈辅助改变为循环。
基本思路:
1.栈里面取一段区间单趟排序
2.单趟分割子区间入栈
3.子区间仅存在一个值或者不存在时就不入栈
void QuickSortNonr(int* arr, int left, int right) { //建栈 stack st; StackInit(&st); StackPush(&st, left); StackPush(&st, right); while (!StackEmpty(&st)) { right = StackTop(&st); StackPop(&st); left = StackTop(&st); StackPop(&st); //前后指针法排序 int key = PointSort(arr, left, right); if (key+1 < right) { StackPush(&st, key + 1); StackPush(&st, right); } if (left < key-1) { StackPush(&st, left); StackPush(&st, key-1); } } StackDestory(&st); }
归并排序
归并排序是建立在归并操作下的一个有效排序,该算法是采用分治法的一个典型用例。
归并排序递归
方法:利用递归将一段序列分成俩段,将每一段的子序列有序,将俩个有序表合成一个有序表,称为二路合并。
思路:
1.建立一个临时数组,只使用一个临时数组
2.相当于后续遍历:先递归后归并
3.俩个有序区间归并,依次比较,小的尾插到新空间
//归并排序 void MergeSort(int* arr, int n) { int* tmp = (int*)malloc(sizeof(int) * (n)); if (tmp == NULL) { perror("malloc fail"); return; } _MergeSort(arr, 0, n - 1, tmp); free(tmp); tmp = NULL; } //归并子排序 void _MergeSort(int* arr, int left, int right, int* tmp) { if (left >= right) { return; } int mid = (right + left) / 2; //后续递归 _MergeSort(arr, left, mid, tmp); _MergeSort(arr, mid + 1, right, tmp); //归并 int begin1 = left; int end1 = mid; int begin2 = mid + 1; int end2 = right; int i = left; while (begin1 <= end1 && begin2 <= end2) { if (arr[begin1] < arr[begin2]) { tmp[i++] = arr[begin1++]; } else { tmp[i++] = arr[begin2++]; } } while (begin1 <= end1) { tmp[i++] = arr[begin1++]; } while (begin2 <= end2) { tmp[i++] = arr[begin2++]; } for (i = 0; i <= (right-left); i++) { arr[left + i] = tmp[left + i];
归并排序非递归
归并排序非递归使用分组进行排序,即先局部后去全部。
代码思路:
1.以gap为单位,每俩组进行归并排序
2.考虑归并排序后将创建的新数组统一赋值给原数组或者分批次赋值给原数组
3.gap扩大一倍,循环第一步
4.结束条件以gap小于数组元素为主
5.考虑后面元素以gap为单位时的越界问题
当最后的元素存在越界问题时:进行边界修正(复杂问题分解为简单问题:分类处理)
1.end1越界,不归并,直接下移下移
2.begin2越界,end2未越界,不归并,直接下移
3.end2越界。end1与begin2未越界,继续归并,修正end2.
//归并排序非递归 void MergeSortNonr(int* arr, int n) { int gap = 1; int i = 0; int* tmp = (int*)malloc(sizeof(int) * n); if (tmp == NULL) { perror("malloc fail"); return; } while (gap < n) { for (i = 0; i < n; i += gap * 2) { int begin1 = i; int end1 = i + gap - 1; int begin2 = i + gap; int end2 = i + gap * 2 - 1; int j = begin1; //越界 if (end1 >= n || begin2 >= n) { break; } if (end2 >= n) { end2 = n - 1; } printf("[%d,%d][%d %d] ", begin1, end1, begin2, end2); //进行归并排序 while (begin1 <= end1 && begin2 <= end2) { if (arr[begin1] < arr[begin2]) { tmp[j++] = arr[begin1++]; } else { tmp[j++] = arr[begin2++]; } } while (begin1 <= end1) { tmp[j++] = arr[begin1++]; } while (begin2 <= end2) { tmp[j++] = arr[begin2++]; } memcpy(arr + i, tmp + i, (end2-i+1) * sizeof(int)); } printf("\n"); gap *= 2; } free(tmp); tmp = NULL; }
非比较排序
非比较排序中有:
1.计数排序
2.基数排序(实际应用较少)
3.桶排序(设计原因)
这里主要介绍计数排序:
计数排序又称为鸽巢排序,是对哈希直接定址法的变形应用。
步骤:
1.统计每个数据出现的次数
此时遍历以便原数组——时间复杂度为O(N)
2.进行排序
此时遍历一遍计数数组——时间复杂度为O(Max),此时的时间复杂度Max,为原数组中的最大值。
假设有一个数组为100,102,100,101,103,105,105,101,101。那么使用这个数组利用绝对位置会浪费很大空间,可以将数组中的最大值减去最小值来确定数组的元素相对位置的范围。
如图,0~100之间的数组位置将会被浪费。
利用相对位置映射计数,可以将时间复杂度改为O(range),同时也可以进行计负数。
代码思路:
1.找原数组中的max、min求得range
2.设置计数数组counta(新建),并初始化为0(使用malloc+memset或者calloc)
3.统计次数
4.排序原数组
//计数排序 void CountSort(int* arr, int n) { //找最大值与最小值 int min = arr[0]; int max = arr[0]; int i = 0; for (i = 0; i < n; i++) { if (min > arr[i]) { min = arr[i]; } if (max < arr[i]) { max = arr[i]; } } int range = max - min + 1; int* tmp = (int*)calloc(range, sizeof(int)); if (tmp == NULL) { perror("calloc fail"); return 0; } //统计元素个数 for (i = 0; i < n; i++) { tmp[arr[i] - min]++; } //排序原数组 int j = 0; for (i = 0; i < range; i++) { while (tmp[i] > 0) { arr[j++] = i + min; tmp[i]--; } } free(tmp); tmp = NULL; }
总结:计数排序适合范围集中,且范围不大的整型数组进行排序,不适合范围分散或者非整型的排序,例如:字符串、浮点数、结构体等等。
计数排序的时间复杂度为:O(N+range)
计数排序的空间复杂度为:O(range)
排序算法总结(复杂度与稳定性)
简单描述一下稳定性:稳定性,即排序之后可以保证相同的俩个元素的相对位置在进行排序后依旧相同。比如一个数组中有俩个10,排序前的10在10前面,排序后前后位置相同。
稳定性在实际生活中的应用:
1.相同分数先交卷的在前面
2.综合成绩相同,语文成绩居高者在前面,即先进行语文排序,确定相对位置,再进行综合排序。
- 冒泡排序:稳定
保证相等时不换即可稳定 - 简单选择排序:不稳定
有些情况会被干扰
- 直接插入排序:稳定
- 希尔排序:不稳地
相同的数据可能由于gap的不同,将元素放置在不同的组进行排序 - 堆排序:不稳地
- 归并排序:稳定
- 快速排序:不稳定
总结:在比较快的排序中只有归并排序是稳定的,其他稳定的还有冒泡排序与直接插入排序。