参考:https://blog.csdn.net/jiaoyangwm/article/details/80808235
https://blog.csdn.net/a2392008643/article/details/81781766
https://mp.weixin.qq.com/s/vn3KiV-ez79FmbZ36SX9lg
本文仅是将他人博客经个人理解转化为简明的知识点,供各位博友快速理解记忆,并非纯原创博客,如需了解详细知识点,请查看参考的各个原创博客。
目录
第十章 排序
10.1 排序算法综述
内排序与外排序:
根据排序过程中,待排序记录是否全部被放置在内存中,排序分为:内排序和外排序
- 内排序:排序过程中,待排序的所有记录全部被放置在内存中(主要介绍内排序)
- 外排序:排序过程中,待排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次进行数据交换才可以。
排序的稳定性:
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
10.2 冒泡排序
算法步骤:
- 从前往后比较相邻的两个元素,如果第一个比第二个大,就交换他们两个,进行length-1轮;
- 上一步做完i轮后,最后的i元素会是有序且最大的数。针对除最后i个外的所有的元素重复以上的步骤直到没有任何一对数字需要比较。
算法代码:
void BubbleSort(int* arr, int length) {
//外层是比较次数,总共要经过 N-1 轮比较
for (int i = 1; i < length; i++) {
//设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已经完成
bool flag = true;
for (int j = 0; j < length - i; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]);
flag = false;
}
}
if (flag) break;
}
}
10.3 选择排序
算法步骤:
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾;
- 重复第二步,直到所有元素均排序完毕。
算法代码:
void SelectionSort(int* arr, int length) {
for (int i = 0; i < length-1; i++) {
// 记录目前能找到的最小值元素的下标
int min = i;
// 每轮需要从i后第一个数字开始比到末尾
for (int j = i+1; j < length; j++) {
if (arr[j] < arr[min]) min = j;
}
// 将找到的最小值和i位置所在的值进行交换
if (i != min) swap(arr[i], arr[min]);
}
}
10.4 插入排序
算法步骤:
- 将第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列;
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面)。
算法代码:
void InsertionSort(int* arr, int length) {
for (int i = 1; i < length; i++) {
// 记录要插入的数据
int tmp = arr[i];
// 从有序序列最右边的开始比较,找到比arr[i]小的数(比大的往右移动)
int j = i - 1;
while (j >= 0 && tmp < arr[j]) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = tmp;
}
}
10.5 希尔排序
利用插入排序的思想,考虑到插入排序在序列基本有序且数量较少时性能较高,因此先对序列进行逻辑上的分组然后再进行插入排序。
算法步骤:
- 设定初始增量t;
- 每趟排序,根据对应的增量 t,将待排序列分割成若干长度为 m 的子序列,分别对各子序列进行直接插入排序;
- 随后减少增量,增加分组,继续对每个子序列进行插入排序,直到增量为1,整个序列作为一个子序列来处理,子序列长度即为整个序列的长度。
算法代码:
void ShellSort(int* arr, int length) {
for (int gap = length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < length; i++) {
// 记录要插入的数据
int tmp = arr[i];
// 从有序子序列最右边的开始比较,找到比arr[i-gap]小的数(比大的往右移动)
int j = i - gap;
while (j >=0 && tmp < arr[j]) {
arr[j + gap] = arr[j];
j -= gap;
}
arr[j + gap] = tmp;
}
}
}
10.6 归并排序
使用分治思想,将原始序列分为两部分分别排序,然后合并,重点在于合并(治)过程。
算法步骤:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动该指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。(这些都是治的过程)
算法代码:
void merge(int* arr, int L, int M, int R) {
//申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
int* tmp = new int[R - L + 1];
int i = 0;
//设定两个指针,最初位置分别为两个已经排序序列的起始位置
int pFirst = L;
int pRight = M + 1;
//比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动该指针到下一位置,重复该步骤直到某一指针达到序列尾
while (pFirst <= M && pRight <= R) {
tmp[i++] = arr[pFirst] < arr[pRight] ? arr[pFirst++] : arr[pRight++];
}
//将另一序列剩下的所有元素直接复制到合并序列尾
while (pFirst <= M) {
tmp[i++] = arr[pFirst++];
}
while (pRight <= R) {
tmp[i++] = arr[pRight++];
}
//将临时数组的数据存入原数组
for (int j = 0; j < (R - L + 1); j++) {
arr[L + j] = tmp[j];
}
}
void MergeSort(int* arr, int L, int R) {
if (L == R) {
return;
}
int mid = (L + R) / 2;
MergeSort(arr, L, mid);
MergeSort(arr, mid + 1, R);
merge(arr, L, mid, R);
}
void MergeSort(int* arr, int length) {
if (arr == nullptr || length<2) {
return;
}
MergeSort(arr, 0, length - 1);
}
10.7 快速排序
与归并排序类似,也使用分治思想。
算法步骤:
- 从数列中挑出一个元素,称为 “基准”(pivot),一般选择最后一个元素;
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边);
- 在这个分区退出之后,该基准就处于数列的中间位置,这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
算法代码:
int partition(int* arr, int L, int R) {
//记录基准的值
int pivot = arr[L];
while (L < R) {
//从右往左找到小于基准的值的索引
while (L < R && pivot <= arr[R]) R--;
//将该值与左指针所指位置的值交换
if (L < R) arr[L++] = arr[R];
//从左往右找到大于基准的值
while (L < R && pivot >= arr[L]) L++;
//将该值与右指针所指位置的值交换
if (L < R) arr[R--] = arr[L];
}
//将左右指针重叠位置(索引)替换为基准值,此时该指针左边全小于基准,右边全大于基准
arr[L] = pivot;
//返回分区位置索引
return L;
}
void QuickSort(int* arr, int L, int R) {
if (L >= R) return;
int M = partition(arr, L, R);
QuickSort(arr, L, M - 1);
QuickSort(arr, M + 1, R);
}
void QuickSort(int* arr, int length) {
if (arr == nullptr || length<2) {
return;
}
QuickSort(arr, 0, length - 1);
}
10.8 堆排序
首先,我们列出堆的基本形式:
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
算法步骤:
- 将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点;
- 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
- 然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值,继续交换堆顶元素与当前末尾元素;
- 反复执行调整+交换步骤,直到整个序列有序。
算法代码:
void heapify(int* arr, int i, int length) {
//找到该节点左右两个子节点的索引
int left = 2 * i + 1;
int right = 2 * i + 2;
//记录值最大节点的索引
int largest = i;
//如果左子节点大于该节点,则将值最大索引改为左子节点索引
if (left < length && arr[left] > arr[largest]) {
largest = left;
}
//如果右子节点大于该节点,则将值最大索引改为右子节点索引
if (right < length && arr[right] > arr[largest]) {
largest = right;
}
//如果值最大索引被改动过,则替换两个值,并对替换后的子节点也进行最大堆调整
if (largest != i) {
swap(arr[largest], arr[i]);
heapify(arr, largest, length);
}
}
void HeapSort(int* arr, int length) {
//1.构建最大堆
for (int i = length / 2 - 1; i >= 0; i--) {
//从第一个非叶子结点开始,从下至上,从右至左调整结构
heapify(arr, i, length);
}
//2.交换堆顶元素与末尾元素+调整剩余堆结构
for (int j = length - 1; j > 0; j--) {
swap(arr[0], arr[j]);
heapify(arr, 0, j);
}
}
10.9 计数排序
算法步骤:
- 花O(n)的时间扫描一下整个序列 A,获取最小值 min 和最大值 max;
- 开辟一块新的空间创建新的数组 B,长度为 ( max - min + 1) ;
- 数组 B 中 index 的元素记录的值是 A 中某元素出现的次数;
- 最后输出目标整数序列,具体的逻辑是遍历数组 B,输出相应元素以及对应的个数。
算法代码:
void CountSort(int* arr, int length) {
int max = arr[0];
int lastIdx = 0;
//1.找到数组中的最大值
for (int i = 1; i < length; i++) {
max = arr[i] > max ? arr[i] : max;
}
//2.开辟一块空间,将数组中数字出现次数记录入空间内
int* sortArr = new int[max + 1]();
for (int j = 0; j < length; j++) {
sortArr[arr[j]]++;
}
//3.把空间内的值输入到原数组中
for (int k = 0; k < max + 1; k++) {
while (sortArr[k] > 0) {
arr[lastIdx++] = k;
sortArr[k]--;
}
}
}
10.10 桶排序
桶排序是: 桶思想排序 + 一个普通的排序(常用快速排序)。
算法步骤:
- 找到数组中最大最小值,以此获得桶的个数((maxValue - minValue) / bucketSize + 1);
- 将数组数据映射入相应桶中;
- 对每个桶进行排序,并将排序后的数据插入原数组中。
算法代码:
void BucketSort(int* arr, int length, int bucketSize) {
if (length < 2) return;
// 找到数组中最大最小值
int minValue = arr[0], maxValue = arr[0];
for (int i = 0; i < length; i++) {
minValue = arr[i] < minValue ? arr[i] : minValue;
maxValue = arr[i] > maxValue ? arr[i] : maxValue;
}
//得到桶的个数
int bucketCnt = (maxValue - minValue) / bucketSize + 1;
vector<vector<int>> buckets(bucketCnt);
// 利用映射函数将数据分配到各个桶中
for (int j = 0; j < length; j++) {
int index = (arr[j] - minValue) / bucketSize;
buckets[index].push_back(arr[j]);
}
int arrIdx = 0;
for (auto bucket : buckets) {
if (bucket.size() <= 0) continue;
//对每个桶进行排序(此处采用插入排序)
int* tmpArr = new int[bucket.size()]();
copy(bucket.begin(), bucket.end(), tmpArr);
InsertionSort(tmpArr, bucket.size());
//将桶内元素写入原数组
for (int t = 0; t < bucket.size(); t++) {
cout << tmpArr[t] << endl;
arr[arrIdx++] = tmpArr[t];
}
}
}
10.11 基数排序
桶排序是: 桶思想排序 + 一个普通的排序(常用快速排序)。
算法步骤:
- 取得数组中的最大数,并取得位数,将所有待比较数值(正整数)统一为同样的位数长度,位数较短的在前面补零;
- 从最低位开始,依次进行计数排序;
- 从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
算法代码:
//拿到传入数的位数
int getRadixCount(int count) {
int num = 1;
if (count / 10 >0) {
num++;
}
return num;
}
void RadixSort(int* arr, int length) {
int max = arr[0];
for (int i = 1; i< length; i++) {
max = arr[i]>max ? arr[i] : max;
}
int radixCount = getRadixCount(max);
int mod = 10, dev = 1;
//遍历和最大值位数相等的次数(计数排序)
for (int i = 0; i < radixCount; i++, mod *= 10, dev *= 10) {
// 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
vector<vector<int>> counter(mod * 2);
for (int j = 0; j < length; j++) {
int bucket = ((arr[j] % mod) / dev) + mod;
counter[bucket].push_back(arr[j]);
}
int pos = 0;
for (auto bucket : counter) {
for (int value : bucket) {
arr[pos++] = value;
}
}
}
}
10.12 相关面试题
Q:
A: