目录
给大家推荐一款刷题,找工作的好网站——牛客网
牛客网 - 找工作神器|笔试题库|面试经验|实习招聘内推,求职就业一站解决_牛客网
插入排序
思路(这里以升序为例):
1.先用一个变量接收最后一个元素,这里是tmp
2.用tmp保存的元素与前面的进行比较,如果比前面小则把前面的覆盖到end+1的位置
3.覆盖之后end继续向前走,end+1,自然也跟着向前走,此时可以看到tmp的作用,就是保存最后一个元素
4.当end<0时,一轮比较结束,此时将tmp的值传给end+1,即队首的位置,还有一种情况可使一轮比较结束
当end位置的元素<tmp时,一轮比较结束,进行下一轮比较
void InsertSort(int* a, int n) { int end = n - 1; int tmp = a[end + 1]; for (int i = 0; i < n-1; i++) { end = i; tmp = a[end + 1]; while (end >= 0) { if (a[end] > tmp) { a[end + 1] = a[end]; end--; } else break; } a[end + 1] = tmp; } } int main() { int arr[] = {9,8,7,6,5,4,3,2,1,0 }; int n = sizeof(arr) / sizeof(arr[0]); InsertSort(arr, n); return 0; }
直接排序时间复杂度:O(N^2)
希尔排序
1.预排序:接近有序,将间隔为gap的数据分成一组
设置一个gap将数组分为几组,这里gap是3,将数组分为了3组
9 5 8 5一组,1 7 6一组,2 4 3 一组
我们先将第一个红色部分的进行排序,我们可以看到红色部分是按照升序的
void ShellSort(int* a, int n) { int gap = 3; int end = n - 1; int tmp = a[end + gap]; for (int i = 0; i < n-gap; i+=gap) { end = i; tmp = a[end + gap]; while (end >= 0) { if (a[end] > tmp) { a[end + gap] = a[end]; end=end-gap; } else break; } a[end + gap] = tmp; } } int main() { int arr[] = {9,1,2,5,7,4,8,6,3,5 }; int n = sizeof(arr) / sizeof(arr[0]); InsertSort(arr, n); return 0; }
2.对3个组分别进行排序,使这三个部分有序
void ShellSort(int* a, int n) { int gap = 3; for (int j = 0; j < gap; ++j) { for (int i = j; i < n - gap; i += gap) { int end = i; int tmp = a[end + gap]; while (end >= 0) { if (a[end] > tmp) { a[end + gap] = a[end]; end -= gap; } else { break; } } a[end + gap] = tmp; } } } int main() { int arr[] = {9,1,2,5,7,4,8,6,3,5 }; int n = sizeof(arr) / sizeof(arr[0]); ShellSort(arr, n); return 0; }
也可以这样
void ShellSort(int* a, int n) { int gap = 3; for (int i = 0; i < n - gap; i ++) { int end = i; int tmp = a[end + gap]; while (end >= 0) { if (a[end] > tmp) { a[end + gap] = a[end]; end -= gap; } else { break; } } a[end + gap] = tmp; } } int main() { int arr[] = {9,1,2,5,7,4,8,6,3,5 }; int n = sizeof(arr) / sizeof(arr[0]); ShellSort(arr, n); return 0; }
3.对整体进行排序
void ShellSort(int* a, int n) { int gap = n; while (gap > 1) { gap = gap / 3 + 1;//或gap=gap/2; for (int i = 0; i < n - gap; i++) { int end = i; int tmp = a[end + gap]; while (end >= 0) { if (a[end] > tmp) { a[end + gap] = a[end]; end -= gap; } else { break; } } a[end + gap] = tmp; } } } int main() { int arr[] = {9,1,2,5,7,4,8,6,3,5 }; int n = sizeof(arr) / sizeof(arr[0]); ShellSort(arr, n); return 0; }
当gap是1的时候是直接排序,其余情况是预排序
希尔排序时间复杂度:O(N^1.3)
选择排序
// 最坏时间复杂度:O(N^2) // 最好时间复杂度:O(N^2) void SelectSort(int* a, int n) { int begin = 0, end = n - 1; while (begin < end) { // 选出最小的放begin位置 // 选出最大的放end位置 int mini = begin, maxi = begin; for (int i = begin + 1; i <= end; ++i) { if (a[i] > a[maxi]) { maxi = i; } if (a[i] < a[mini]) { mini = i; } } Swap(&a[begin], &a[mini]); // 修正一下maxi if (maxi == begin) //如果没有这条语句,当最大数是首元素时,前面语句会把最小的元素换到首元素,而此时maxi指向首元素,首元素却不是最大值,后面再交换就会出错 maxi = mini; Swap(&a[end], &a[maxi]); ++begin; --end; } }
1.一开始让maxi 和 mini 为数组首元素的下标
2. 接着遍历数组,进行比较替换
直接选择排序最慢的排序之一
选择排序的时间复杂度不像前面几种排序方法那样,前面几种排序方法的时间复杂度不是一眼就能看出来的,而是要通过推导计算才能得到的。一般会涉及到递归和完全二叉树,所以推导也不是那么容易。但是选择排序就不一样了,你可以很直观的看出选择排序的时间复杂度:就是两个循环消耗的时间;
比较时间:T = (n-1))+ (n -2)+(n - 3).... + 1; ===>> T = [n*(n-1) ] / 2;
交换时间:最好的情况全部元素已经有序,则 交换次数为0;最差的情况,全部元素逆序,就要交换 n-1 次;
所以最优的时间复杂度 和最差的时间复杂度 和平均时间复杂度 都为 :O(n^2)2.使用选择排序对长度为100的数组进行排序,则比较的次数为( )
A.5050
B.4950
C.4851
D.2475
答案:B
解析:
选择排序,每次都要在未排序的所有元素中找到最值,
如果有n个元素,则
第一次比较次数: n - 1
第二次比较次数: n - 2
....
第n - 1次比较次数: 1
所有如果n = 100
则比较次数的总和:99 + 98 + ...... + 1
共4950次。
交换排序
冒泡排序
void BubbleSort(int* a, int n) { for (int j = 0; j < n; ++j) { int exchange = 0; for (int i = 1; i < n - j; ++i) { if (a[i - 1] > a[i]) { Swap(&a[i - 1], &a[i]); exchange = 1; } } if (exchange == 0) { break; } } }
比较集中排序,这里十万个数据,冒泡排序很慢
冒泡排序:最坏情况时间复杂度:O(N^2),最好情况O(N)
快速排序
hoare版本
1.设置最左边(或最右边)一个值为key,把比key小的key的放在key左边,大于key的放在key右边,这里key是6
2.如何保证相遇位置比key要小,让R先走即可,这种情况是让左边第一个做key,同理右边第一个做key,让L先走,即可在小于K处相遇
3.相遇之后交换相遇位置的数和key所指的数即可
int PartSort(int* arr, int left, int right) { int keyi = left; while (left < right) { while (left < right && arr[right] >= arr[keyi])//left<right是为了防止这俩个错过 { right--; } while (left < right && arr[left] <= arr[keyi]) { left++; } if (left<right) Swap(&arr[left], &arr[right]); } int meeti = left; Swap(&arr[meeti], &arr[keyi]); return meeti; }
单趟排序
要对所有数据排序,进行递归即可
如何递归?
将keyi左边先递归排序,然后再右边
int PartSort(int* arr, int left, int right) { int keyi = left; while (left < right) { while (left < right && arr[right] >= arr[keyi])//left<right是为了防止这俩个错过 { right--; } while (left < right && arr[left] <= arr[keyi]) { left++; } if (left<right) Swap(&arr[left], &arr[right]); } int meeti = left; Swap(&arr[meeti], &arr[keyi]); return meeti; } //部分排序 void QuickSort(int* a, int begin, int end) { if (begin >= end) { return; //如果左指针>=右指针,返回 } int keyi=PartSort(a, begin, end); QuickSort(a, begin, keyi - 1);// QuickSort(a, keyi+1, end); } //快排 int main() { int arr[] = {9,1,2,5,7,4,8,6,3,5 }; int n = sizeof(arr) / sizeof(arr[0]); QuickSort(arr, 0, n - 1); return 0; }
当每次选key为中间值时,递归深度:logN,每次单趟排序合计起来个数是N,时间复杂度:O(N*logN)。
快排比堆排,希尔排序略胜一筹
单趟排序,如果选中间做key,如果还有一个大小为key的值在key的左边或右边,就不好搞。
如果有序/接近有序,选左边排序,比key大的在左边,没有比key小的,key的左边为空,之后递归右边
如果这样一直往下走,树的高度是N,每次递归都会少一个数
时间复杂度:O(N^2)
debug,N=10000,就会栈溢出,此时换realse(100000个数据)版本看O(N^2)情况下的运算情况
100W个数据
优化选key逻辑:1.随机选一个位置做key(如果每次选最左边或最右边有可能会发生跟上面O(N^2)一样的情况,但如果随机选位置就算有序,每次选的数不一定是最左边或最右边)
2.针对有序情况,选正中间的值做key,之后把key和最左边或最右边的交换即可
3.三数取中,第一个位置,中间位置,和最后一个位置,选出这三个位置上的中间数据值,然后再跟最左边数据交换
void Swap(int* p1, int* p2) { int tmp = *p1; *p1 = *p2; *p2 = tmp; } int GetMindIndex(int* arr, int left, int right) { int mid = (right + left) / 2; if (arr[left] < arr[mid]) { if (arr[mid] < arr[right]) return mid; else if (arr[right]<arr[left]) return left; else return right; } else { if (arr[mid] > arr[right]) return mid; else if (arr[right] > arr[left]) return left; else return right; } } int PartSort(int* arr, int left, int right) { int mid = GetMindIndex(arr, left, right); Swap(&arr[mid], &arr[left]); int keyi = left; while (left < right) { while (left < right && arr[right] >= arr[keyi])//left<right是为了防止这俩个错过 { right--; } while (left < right && arr[left] <= arr[keyi]) { left++; } if (left<right) Swap(&arr[left], &arr[right]); } int meeti = left; Swap(&arr[meeti], &arr[keyi]); return meeti; } void QuickSort(int* a, int begin, int end) { if (begin >= end) { return; } int keyi = PartSort(a, begin, end); QuickSort(a, begin, keyi - 1);// QuickSort(a, keyi + 1, end); } int main() { int arr[] = { 9,1,2,5,7,4,8,6,3,5 }; int n = sizeof(arr) / sizeof(arr[0]); QuickSort(arr, 0, n - 1); return 0; }
测试1000W个数据
跟上面O(N^2)的相比,提高了不少,不用害怕栈溢出,因为其深度不会太深 ,深度是logN
小区间优化
倒数三层合计占用80%栈帧
这种形态有点像二叉树,最后一层占了一半的调用次数,每次调用就会开辟栈帧,如果倒数第二层有8个数进行排序,则要开辟三层栈帧, 8个值本身就是小范围,调用三层栈帧,调用7-8次递归,比较浪费栈帧空间。付出的代价太大
因此当数据<=8时,我们用插入排序,其余用快排
void Swap(int* p1, int* p2) { int tmp = *p1; *p1 = *p2; *p2 = tmp; } int GetMindIndex(int* arr, int left, int right) { int mid = (right + left) / 2; if (arr[left] < arr[mid]) { if (arr[mid] < arr[right]) return mid; else if (arr[right]<arr[left]) return left; else return right; } else { if (arr[mid] > arr[right]) return mid; else if (arr[right] > arr[left]) return left; else return right; } } int PartSort(int* arr, int left, int right) { int mid = GetMindIndex(arr, left, right); Swap(&arr[mid], &arr[left]); int keyi = left; while (left < right) { while (left < right && arr[right] >= arr[keyi])//left<right是为了防止这俩个错过 { right--; } while (left < right && arr[left] <= arr[keyi]) { left++; } if (left<right) Swap(&arr[left], &arr[right]); } int meeti = left; Swap(&arr[meeti], &arr[keyi]); return meeti; } void InsertSort(int* a, int n) { for (int i = 0; i < n -1; i++) { int end = i; int tmp = a[end +1]; while (end >= 0) { if (a[end] > tmp) { a[end +1] = a[end]; end --; } else { break; } } a[end +1] = tmp; } } void QuickSort(int* a, int begin, int end) { if (begin >= end) { return; } if (end - begin <= 8) { InsertSort(a + begin, end - begin + 1); //a+begin是让从左边开始,每次递归都会导致左边不一样 } else { int keyi = PartSort(a, begin, end); QuickSort(a, begin, keyi - 1);// QuickSort(a, keyi + 1, end); } } int main() { int arr[] = { 9,1,2,5,7,4,8,6,3,5 }; int n = sizeof(arr) / sizeof(arr[0]); QuickSort(arr, 0, n - 1); return 0; }
我们可看到这种写法有所优化
挖坑法
1. 先创建一个变量将坑位的值保留,建立俩个指针,分别指向最左边和最右边,若左边为坑,则右指针先走,反之,左指针先走
2.当右指针遇到比key小的数字,停下来,并把该数字放到hole所指位置 (即填到坑里),自己形成新的坑位
3.然后让左边走,遇到比key大的停下来,把数字填到坑里,然后自己相乘新坑位
void InsertSort(int* a, int n) { for (int i = 0; i < n -1; i++) { int end = i; int tmp = a[end +1]; while (end >= 0) { if (a[end] > tmp) { a[end +1] = a[end]; end --; } else { break; } } a[end +1] = tmp; } } void QuickSort(int* a, int begin, int end) { if (begin >= end) { return; } if (end - begin <= 8) { InsertSort(a + begin, end - begin + 1); } else { int keyi = PartSort2(a, begin, end); QuickSort(a, begin, keyi - 1);// QuickSort(a, keyi + 1, end); } } //挖坑法 int PartSort2(int* arr, int left, int right) { int mid = GetMindIndex(arr, left, right); Swap(&arr[mid], &arr[left]); 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; }
性能跟之前的差不多
前后指针法
1.设置俩个指针和一个key,key用来保存数据,俩个指针一前一后,prev在后,cur在前
2.cur先走,prev接着走,当prev的后一个数字比key大时(也就是cur遇到比key大的数字),prev停下来,cur,继续走,cur遇到比key小的值停下来
3.停下来之后,prev向前走一步,交换俩数字,之后按照这种方法继续走就行,当cur走到数组之外时,停止,prev会在小于key的位置停下
int PartSort3(int* arr, int left, int right) { int mid = GetMindIndex(arr, left, right); Swap(&arr[mid], &arr[left]); int keyi = left; int cur = left + 1; int prev = left; while (cur <= right) { if (arr[cur] < arr[keyi] && ++prev != cur)//等于可以不用动那个值,让他留在哪里 { Swap(&arr[cur], &arr[prev]); } ++cur; } Swap(&arr[keyi], &arr[prev]); return prev; } int main() { int arr[] = { 9,1,2,5,7,4,8,6,3,5 }; int n = sizeof(arr) / sizeof(arr[0]); QuickSort(arr, 0, n - 1); return 0; }
非递归
使用递归方式进行快排时,若递归层数较多,则会造成栈溢出,我们可以用数据结构的“栈”来用非递归实现
先把起点和终点的下标放进去,这样相当于把第一行的,然后用left和right变量来接收这俩个值,并把这俩个值出栈left=0,right=9
出栈后,进行排序,并返回他们中间数据的下标,由于我们递归的时候是先递归左边,再递归右边,所以用栈实现的时候也是先对左边排序,再对右边排序,但由于栈是先入后出,所以要先把右边压栈,再把左边压栈,这样出去的时候就是左先出,先计算左,然后右出,再计算右,压栈的时候左和右要和程序里面的left和right相对应
我们这里在设计的时候是先给右赋值,后给左赋值,再结合我们先给左半边排序再给右半边排序的规则,我们先压5,再压9,之后先压0,再压3
当中间值下标+1>=右边或者左边+1>=中间值下标的时候我们不需要压栈
比如这里最后一行最左边0 中间值下标 0 最右边 0,如果进行压栈待会出栈会出来2个0,而数组里面只有一个0,会影响程序的正确性
int PartSort3(int* arr, int left, int right) { int mid = GetMindIndex(arr, left, right); Swap(&arr[mid], &arr[left]); int keyi = left; int cur = left + 1; int prev = left; while (cur <= right) { if (arr[cur] < arr[keyi] && ++prev != cur)//等于可以不用动那个值,让他留在哪里 { Swap(&arr[cur], &arr[prev]); } ++cur; } Swap(&arr[keyi], &arr[prev]); return prev; } void QuickSort(int* a, int begin, int end) { if (begin >= end) { return; } if (end - begin <= 8) { InsertSort(a + begin, end - begin + 1); } else { int keyi = PartSort3(a, begin, end); QuickSort(a, begin, keyi - 1);// QuickSort(a, keyi + 1, end); } } void QuickSortNonR(int* a, 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 keyi = PartSort3(a, left, right); // [left, keyi-1] keyi [keyi+1,right] if (keyi + 1 < right)//限制条件 { StackPush(&st, keyi + 1); StackPush(&st, right); } if (left < right - 1) //限制条件 { StackPush(&st, left); StackPush(&st, keyi - 1); } } StackDestory(&st); } int main() { int arr[] = {1,2,3,4,5,6,7,8,9,10 }; int n = sizeof(arr) / sizeof(arr[0]); QuickSortNonR(arr, 0, n - 1); return 0; }