1.排序的概念及其运用
1.1排序的概念
1.排序:排序简而言之就是使一串记录,按照其中某个或者某些关键词的大小对其进行升序或降序排列
2.稳定性:在一串序列中需要根据某些关键词大小进行排序的序列,但两个关键词大小相同,在不将原来序列中关键词大小相同的两个的前后顺序调换的排序就是稳定的
3.内部排序:数据元素全部放在内存中的排序
4.外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不断地在内外存之间移动数据的排序。
1.2排序的运用
在生活中很多都需要用到排序算法,比如学生成绩的排序,手机销量的排序,抖音热榜的排序
2.常见的排序算法
2.1冒泡排序
算法思想:
将最大或者最小的数据元素排到最后
算法实现:
void BubbleSort(int* a, int n) { int flag = 0; for (int i = 0; i < n - 1; i++) { flag = 0; for (int j = 0; j < n - i - 1; j++) { if (a[j] > a[j + 1]) { Swap(&a[j], &a[j + 1]); flag = 1; } } if (flag == 0) { break; } } }
复杂度分析
时间复杂度:
最好:O(N)
最坏:O(N^2)
空间复杂度:O(1)
2.2选择排序
算法思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,起始位置向后移,直到全部待排序的数据元素排完 。
这里我将其优化了一下,及在待排序序列中找到最大和最小的数据元素,分别将其排到序列的起始位置和最后位置。
算法实现:
void SelectSort(int* a, int n) { int end = n - 1; int begin = 0; while (begin < end) { int mini = begin; int maxi = begin; for (int i = begin + 1; i <= end; i++) { if (a[mini] > a[i]) { mini = i; } if (a[maxi] < a[i]) { maxi = i; } } Swap(&a[mini], &a[begin]); if (maxi == begin) { maxi = mini; } Swap(&a[maxi], &a[end]); end--; begin++; } }
复杂度分析
时间复杂度
最好:O(N^2)
最坏:O(N^2)
空间复杂度:O(1)
2.3直接插入排序
算法思想:
将为排序的数据元素插入到已排序序列中进行排序,如:排升序时,排到第i个元素时,前面i-1个元素已经排好序了,将第i个元素与第i-1个元素相比较,如果大于第i-1个元素,就不需要继续比较,如果小于第i-1个元素,交换两个元素,在将其于第i-2个元素比较,以此类推直到整个序列排序完成。
算法实现:
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 (tmp < a[end]) { a[end + 1] = a[end]; end--; } else { break; } } a[end + 1] = tmp; } }
复杂度分析
时间复杂度:
最好:O(N)
最坏:O(N^2)
空间复杂度:O(1)
2.4希尔排序
算法思想:
希尔排序的思想和直接插入排序的思想有异曲同工之妙,希尔排序就是在直接插入排序上做了一些优化,先对代拍序列进行预排序,减小预排序之间gap的大小,知道为1,为直接插入排序,但由于序列i已经部分有序,直接插入排序的时间复杂度也大幅度降低
算法实现:
void ShellSort(int* a, int n) { int gap = n; while (gap > 1) { gap = gap/3 + 1; 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; } } }
复杂度分析
时间复杂度:
最好:O(NlogN)(具体来说是O(N^1.3)根据大量实验测得)
最坏:O(N^2)
空间复杂度·:O(1)
2.5快速排序
2.5.1hoare版本
算法思想:
定义两个变量left和right,分别指向区间的开头和结尾,定义一个变量key值,key值为数组所在区间的第一个值a[left],在从right开始想左找比key值小的元素,找到了再从left开始向右找比key值大的元素,找到了在将两个数交换,直到left>=right就跳出循环
算法实现:
void QuickSort(int* a , int left, int right) { if (left >= right) { return; } int begin = left; int end = right; int keyi = left; while (left < right) { while (left < right && a[right] >= a[keyi]) { right--; } while (left < right && a[left] <= a[keyi]) { left++; } Swap(&a[left],&a[right]); } Swap(&a[keyi], &a[left]); keyi = left; QuickSort(a, begin, keyi - 1); QuickSort(a, keyi + 1, end); }
但这个快速排序还有一个弊端,在其为降序要将其排为升序时,时间复杂度为O(N^2),于是就可以有两种方法将其时间复杂度稳定在O(NlogN)
1.随机数法:
void QuickSort(int* a , int left, int right) { if (left >= right) { return; } int begin = left; int end = right; int randi = rand() % (right - left + 1); randi += left; Swap(&a[left], &a[randi]); int keyi = left; while (left < right) { while (left < right && a[right] >= a[keyi]) { right--; } while (left < right && a[left] <= a[keyi]) { left++; } Swap(&a[left],&a[right]); } Swap(&a[keyi], &a[left]); keyi = left; QuickSort(a, begin, keyi - 1); QuickSort(a, keyi + 1, end); }
2.三数取中法(加了小区间优化):
int getmid(int* a, int left, int right) { int mid = (left + right) / 2; if (a[mid] < a[left]) { if (a[right] < a[mid]) { return mid; } else if (a[left] < a[right]) { return left; } else { return right; } } else { if (a[right] > a[mid]) { return mid; } else if (a[right] < a[left]) { return left; } else { return right; } } } void QuickSort(int* a, int left, int right) { if (left >= right) { return; } //小区间优化 if (right - left + 1 < 10)//可以减少90%左右的递归 { InsertSort(a + left, right - left + 1); return; } int begin = left; int end = right; int midi = getmid(a, left, right); Swap(&a[midi], &a[left]); int keyi = left; while (left < right) { while (a[right] >= a[keyi] && left < right) { right--; } while (a[left] <= a[keyi] && left < right) { left++; } Swap(&a[left], &a[right]); } Swap(&a[keyi], &a[left]); keyi = left; QuickSort(a, begin, keyi - 1); QuickSort(a, keyi + 1, end); }
2.5.2前后指针法
算法思想:
排升序时:定义两个变量prev,cur,区间的开头和结尾为left,right,prev指向left,cur指left+1,keyi=left,cur小于等于end时,进入循环,当a[cur]>=a[keyi],cur++,否则,prev++,交换a[cur]和a[prev],cur++,直到循环结束,交换a[prev]和a[keyi]
算法实现:
void quicksort(int* a, int left, int right) { while (left >= right) { return; } int keyi = left; int prev = left; int cur = left; while (cur <= right) { if (a[cur] < a[keyi] && ++prev != cur) { swap(&a[cur], &a[prev]); } cur++; } swap(&a[keyi], &a[prev]); keyi = prev; quicksort(a, left, keyi - 1); quicksort(a, keyi + 1, right); }
2.5.3挖坑法
算法思想:
- 选择基准值(Pivot):从待排序序列中挑出一个元素,称为“基准”(pivot),这个元素可以按照不同的策略来选择,例如选择序列的第一个、最后一个、中间一个或者随机选择一个元素。
- 分区(Partition):重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- “挖坑法”的分区过程通常涉及几个指针或“坑”的概念:
- low 指针:指向当前已排序序列(即小于基准值的元素序列)的末尾。
- high 指针:指向未排序序列的开始。
- pivotIndex:基准值原本的位置,可以理解为“坑”的位置,基准值先被移出(放入临时变量中),待分区完成后放回。
- 分区过程中,通过比较和交换,逐步将小于基准值的元素移动到左侧,大于基准值的元素移动到右侧。同时,low 和 high 指针会向中间移动,直到它们相遇或交错。
- “挖坑法”的分区过程通常涉及几个指针或“坑”的概念:
算法实现:
void QuickSort(int* a, int left, int right) { if (left >= right) { return; } int begin = left; int end = right; int key = a[begin]; int keyi = begin; while (left < right) { while (a[right] >= key && left < right) { right--; } a[keyi] = a[right]; keyi = right; while (a[left] <= key && left < right) { left++; } a[keyi] = a[left]; keyi = left; } a[keyi] = key; QuickSort(a, begin, keyi - 1); QuickSort(a, keyi + 1, end); }
2.5.4快速排序的非递归实现
算法思想:
利用来实现对递归左右子区间的存储和弹出,利用弹出的区间进行快徐排序(随便用一种方法)
算法实现(这里用的是C语言手搓的栈):
void QuickSort(int* a, int left, int right) { Stack s; StackInit(&s); StackPush(&s, right); StackPush(&s, left); while (!StackEmpty(&s)) { int begin = StackTop(&s); StackPop(&s); int end = StackTop(&s); StackPop(&s); int prev = begin; int cur = begin + 1; int keyi = begin; while (cur <= end) { if (a[cur] < a[keyi] && ++prev != cur) { Swap(&a[prev], &a[cur]); } cur++; } Swap(&a[prev], &a[keyi]); keyi = prev; if (keyi + 1 < end) { StackPush(&s, end); StackPush(&s, keyi + 1); } if (begin < keyi - 1) { StackPush(&s, keyi - 1); StackPush(&s, begin); } } StackDestroy(&s); }
复杂度分析
时间复杂度:
最好:O(NlogN)
最坏:O(N^2)
空间复杂度:O(logN)(递归了logN层)
2.6归并排序
算法思想:
将区间不断二分,直到区间中只剩一个元素,在将子区间进行归并,用一个临时数组存储起来,然后在将其拷贝到原数组中
算法实现:
void _MergeSort(int* a, int* tmp, int left, int right) { if (left == right) return; int mid = (left + right) / 2; int begin1 = left, end1 = mid; int begin2 = mid + 1, end2 = right; _MergeSort(a, tmp, begin1, end1); _MergeSort(a, tmp, begin2, end2); 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++]; } memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1)); } void MergeSort(int* a, int n) { int* tmp = (int*)malloc(sizeof(int) * n); if (tmp == NULL) { perror("malloc fail!"); return; } _MergeSort(a, tmp, 0, n - 1); free(tmp); tmp = NULL; }
2.7归并排序的非递归实现
算法思想:
归并排序的非递归实现用栈和队列实现都有点复杂,因为在弹出左右子区间将其合并后,再下一次合并时并不知道其之前的区间范围,于是就可以用循环控制两个子区间的距离来实现归并排序
算法实现:
void MergeSortNonR(int* a, int n) { int *tmp = (int*)malloc(sizeof(int) * n); if (tmp == NULL) { perror("malloc fail!"); return; } int gap = 1; while (gap < n) { for (int j = 0; j < n; j += 2 * gap) { int begin1 = j, end1 = begin1 + gap - 1; int begin2 = j + gap, end2 = begin2 + gap - 1; int i = j; if (end1 >= n || begin2 >= n) { break; } if (end2 >= n) { end2 = n - 1; } 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++]; } memcpy(a + j, tmp + j, sizeof(int) * (end2 - j + 1)); } gap *= 2; } free(tmp); tmp = NULL; }
注意事项:
需要注意区间是否越界,当end1和begin2越界时就不需要合并,当end2越界时,就需要将end2改为n-1,再对区间进行合并,以及拷贝时从原数组和临时数组的起始地址
复杂度分析
时间复杂度:
最好:O(NlogN)
最坏:O(NlogN)
空间复杂度:
创建了一个临时数组,空间复杂度为O(N)