void BubbleSort(int* arr, int n) { for (int i = 0; i < n; i++) { int flag = 0; for (int j = 0; j < n - i - 1; j++) { if (arr[j] > arr[j + 1]) { Swap(&arr[j], &arr[j + 1]); flag = 1; } } if (0 == flag) break; } }
时间复杂度:O(N^2)
快速排序是一种二叉树结构的交换排序方式,基本思想:任取待排元素序列中的某元素作为基准值,按照该基准值将待排序列分割成两子序列,左子序列所有元素均小于该基准值,右子序列均大于该基准值,然后在左子序列,和右子序列重复上述过程,直到待排元素符合预期结果。
void QuickSort(int* arr, int left, int right) { if (left >= right)//left 等于 right说明此时子序列里只有一个数据了,若left 大于 right说明此时子序列为空 { return; } //int meet = hoare_QuickSort(arr, left, right); //int meet = hole_QuickSort(arr, left, right); //int meet = lomuto_QuickSort(arr, left, right); QuickSort(arr, left, meet - 1); QuickSort(arr, meet + 1, right); }
时间复杂度:O(nlogn~n^2),在以下查找基准值的方法,是以待排序列首元素位基准值,这种方法存在缺陷,使得快速排序的性能下降,时间复杂度及较高。
情景:
待排序列位有序,降序、升序、所有元素大小相同,这三种场景中将待排序列排序位升序的序列,时间复杂度位O(n^2),挖坑,找基准值的方法后续补充。(我在详细讲解为什么这三种场景,这么找的基准值)
空间复杂度:O(logn ~ n)
int hoare_QuickSort(int* arr, int left, int right) { int key = left; left++; while(left <= right) { while(left <= right && arr[right] > arr[key]) { right--; } while(left <= right && arr[left] < arr[key]) { left++; } if(left <= right) Swap(&arr[left++], &arr[right]--); } Swap(&arr[right], &arr[key]); return right; }
此时走完循环的最后一步,在left7的位置和right3的位置发生交换后,此时right–,left++,若最外层循环的条件是left < right,此时跳出循环,right和key发生交换,交换完的结果不满足左子序列都小于基准值的标准,是一个错误的代码。不止最外层循环的条件必须是 left <= right,包括内层的两个while循环和if判断语句都是避免这种情况的发生,使得最后一次跳出循环right在left的左边,left在right的右边。
如果不等与使最后的left越过right,是用left < right的限制条件,不保证left和right同时所指的数据满足右子序列大于基准值,或左子序列小于基准值,而直接与rgiht交换
找基准值的函数,看似有两层循环,实际上是一层循环,通过left和right遍历数组元素,时间复杂度O(N)
时间复杂度:找相遇点的时间复杂度为O(n)
不断挖坑找坑填坑的过程
int hole_QuickSort(int* arr, int left, int right) { int hole = left; int key = arr[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; return hole; }
不同于hoare版本的找基准值,通过挖坑法,在循环里,当left == right就直接跳出,hoare版本的left和right相等时需要判断,当前它们所指的值比基准值大还是小,而挖坑法就不需要考虑,left和right相等时,同时指向的位置是一个坑,坑内不存在有效数据,最后直接将基准值填坑即可。
定义两个指针prec,cur,它们只负责找比基准值小的元素。
int lomuto_QuickSort(int* arr, int left, int right) { int prev = left; int cur = left + 1; int key = left; while(cur <= right) { if(arr[cur] < arr[key] && ++prev != cur) { Swap(&arr[cur], &arr[prev]); } cur++; } Swap(&arr[prev], &arr[key]); return prev; }
实现非递归的快速排序,需要借组数据结构栈。栈:先进后出
递归版本的快速排序,通过基准值分割左右子序列,定义了left和right来限定左右子序列的取值范围,也是通过left和right来实现对序列的分割。找完待排序列的基准值后,通过栈保存左右子序列的取值范围,来模拟递归的场景。
由于栈的特殊结构,入栈时先入右区间,最后入左区间,出栈的顺序就为左区间、右区间。
[begin, key - 1]和[key + 1, begin]
void QuickSort_NonR(int* arr, int left, int right) { Stack st; StackInit(&st); StackPush(&st, right); StackPush(&st, left); while (st.top) { int begin = StackTop(&st); StackPop(&st); int end = StackTop(&st); StackPop(&st); int prev = begin; int cur = begin + 1; int key = begin; while (cur <= end) { if (arr[cur] < arr[key] && ++prev != cur) { Swap(&arr[prev], &arr[cur]); } cur++; } Swap(&arr[prev], &arr[key]); key = prev; if (begin < key -1) { StackPush(&st, key - 1); StackPush(&st, begin); } if (key + 1 < end) { StackPush(&st, end); StackPush(&st, key + 1); } } StackDestory(&st); }
时间复杂度:O(nlogn)
归并排序是一种稳定的排序算法,该算法采用分治法,通过递归数组进行分半,递归的对两半序列进行排序,通过不断的将两组有序序列合并为一个有序序列
1 3
1 3
和 9
合并为 一个有序序列,1 3 9
6 10
6 10
和 1 3 9
合并为一个有序序列,1 3 6 9 10
,排序完成。void _MergeSort(int* arr, int left, int right, int* temp) { if (left >= right)//递归结束条件 [left, right] {//一但left与right重合或left大于right,结束递归 return; } int mid = (left + right) / 2; _MergeSort(arr, left, mid, temp); _MergeSort(arr, mid + 1, right, temp);//递归,分半 int begin1 = left; int end1 = mid;//第一个序列的区间,左区间 int begin2 = mid + 1; int end2 = right;//第二个序列的区间,右区间 int index = begin1;//临时数组的下标起始,从左区间开始 while (begin1 <= end1 && begin2 <= end2)//对两个有序序列进行排序 { if (arr[begin1] < arr[begin2]) { temp[index++] = arr[begin1++]; } else { temp[index++] = arr[begin2++]; } } while (begin1 <= end1)//避免该区间还有剩余 { temp[index++] = arr[begin1++]; } while (begin2 <= end2)//避免该区间还有剩余 { temp[index++] = arr[begin2++]; } for (int i = left; i <= right; i++)//将临时数组所有元素拷贝到原数组中。 { arr[i] = temp[i]; } } void MergeSort(int* arr, int n) { int* temp = (int*)malloc(sizeof(int) * n); _MergeSort(arr, 0, n - 1, temp); free(temp); }
MergeSort
函数里实现递归,需要在创建一个函数命名为 _MergeSort
,别忘了将临时数组传递过去(left + right) / 2;
,来完成对数组进行分半 [left, mid]和[mid + 1, right]
时间复杂度:O(nlogn)
空间复杂度:O(n)
计数排序:
适用范围
与数据范围集中的情况
只适用于整数排序,不适用小数
数据过多,可能会对内存消耗造成负担
假设现在有一组数据:6 1 2 9 4 2 4 1 4
,
先根据最大值最小值的差值开辟空间,若是根据最大值开辟,当最大值为109万,最小值为100,一共有10个数据,此时根据最大值直接开辟空间会很浪费,109个整形大小的空间,只用来存放10个数据。
1:2次, 2:2次,4:3次, 6:1次, 9:1次。其余没有出现的数据,默认使用0
根据下标打印,下标为1的元素出现2次,打印两个1,下标为2的元素出现两次,打印两个2,依次类推,最终打印的结果为 1 1 2 2 4 4 4 6 9
,这就排序完成。
此时对 100 101 109 105 101 105
,进行计数排序。
更具最大值减去最小值后加1开辟空间,109 - 100 + 1 = 10,给count开辟10个整形大小空间。
统计每个数据出现的个数,根据该数据对应下标存放在count数组里,而此时数组里下标从0开始,待排序列最小值从100开始,无法一一对应。
通过将待排序列的每个元素大小减去最小值 0 1 9 5 1 5
,即可解决上述问题。
0:出现1次, 1:出现2次, 5,出现2次, 9:出现1次
此时,根据下对对应的此时,打印下标的值,和原序列并不相同,既然之前减去最小值,打印的时候加上最小值即可。100 101 101 105 105 109
当待排序列中存在负数,此时还是更具将待排序列中每个元素减去最小值,更具这个值来对应count下标存放它出现的次数。
-5 -3 2 4 2 1
,将每个数据减去最小值,0 2 7 9 2 6
,在根据count的次数取下标时,加上最小值即可。
void CountSort1(int* arr, int n) { int max = arr[0]; int min = arr[0]; for (int i = 1; i < n; i++) { if (arr[i] < min) min = arr[i]; if (arr[i] > max) max = arr[i]; } //确定数组大小 int range = max - min + 1; int* count = (int*)malloc(sizeof(int) * range); if (count == NULL) { perror("malloc"); exit(1); } memset(count, 0, sizeof(int) * range); //统计数组中每个元素出现的次数,并放在对应下标的count数组里 for (int i = 0; i < n; i++) { count[arr[i] - min]++; } int index = 0; for (int i = 0; i < range; i++) { while (count[i]--) { arr[index++] = i + min;//放完数据,index++到下一个位置 } } }
时间复杂度:O(N + range)
空间复杂度:O(range)
10万个数据(单位:ms):
没有看错,很夸张,计数排序的运行结果0ms
100万个数据(单位:ms):
直接选择排序、直接插入排序、冒泡排序性能,没有放出来。
1000万个数据(单位:ms):