网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
4. 交换排序
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排
序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
4.1✨冒泡排序
冒泡排序的原理是:从左到右,相邻元素进行比较。每次比较一轮,就会找到序列中最大的一个或最小的一个。这个数就会从序列的最右边冒出来。
以从小到大排序为例,第一轮比较后,所有数中最大的那个数就会浮到最右边;第二轮比较后,所有数中第二大的那个数就会浮到倒数第二个位置……就这样一轮一轮地比较,最后实现从小到大排序。
// 冒泡排序
void BubbleSort(int\* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int 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^2)
- 空间复杂度:O(1)
- 稳定性:稳定
4.2 ✨快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中
的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右
子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{
if(right - left <= 1)
return;
// 按照基准值对array数组的 [left, right)区间中的元素进行划分
int div = partion(array, left, right);
// 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
// 递归排[left, div)
QuickSort(array, left, div);
// 递归排[div+1, right)
QuickSort(array, div+1, right);
}
上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,同学们在写递归框架时可想想二叉
树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可
将区间按照基准值划分为左右两半部分的常见方式有:
- hoare版本
- 挖坑法版本
- 前后指针版本
4.2.1快速排序hoare版本
一趟的排序动画演示
// 快速排序hoare版本
int PartSort1(int\* a, int left, int right)
{
int keyi = left;
while (left < right)
{
//找小
while (left < right && a[right] >= a[keyi])
{
right--;
}
//找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
if (left < right)
{
//交换
Swap(&a[left], &a[right]);
}
}
Swap(&a[left], &a[keyi]);
return left;
}
void QuickSort(int\* a, int left, int right)
{
if (left >= right)
return;
int pivot=PartSort1(a, left, right);
//排完了一趟后分成了两个区域 [left,pivot] pivot [pivot+1,right];
QuickSort(a, left, pivot - 1);//递归左子区间
QuickSort(a, pivot+1, right);//递归右子区间
}
4.2.2快速排序挖坑法版本
一趟的排序动画演示
// 快速排序挖坑法
int PartSort2(int\* a, int left, int right)
{
int key = a[left];//保存基准值
int hole = left;//坑位
while (left < right)
{
//找小
while (left < right && a[right] >= key)
{
right--;
}
//放坑位
if (left < right)
{
a[hole] = a[right];
hole = right;//更新坑位
}
//找大
while (left < right && a[left] <= key)
{
left++;
}
if (left < right)
{
a[hole] = a[left];
hole = left;
}
}
a[hole] = key;
return hole;
}
void QuickSort(int\* a, int left, int right)
{
if (left >= right)
return;
int pivot=PartSort2(a, left, right);
//排完了一趟后分成了两个区域 [left,pivot] pivot [pivot+1,right];
QuickSort(a, left, pivot - 1);//递归左子区间
QuickSort(a, pivot+1, right);//递归右子区间
}
4.2.3快速排序前后指针版本
// 快速排序前后指针法
int PartSort3(int\* a, int left, int right)
{
int keyi = left;
int prev = left;
int cur = left+1;
while (cur <= right)
{
//cur从左往右找小跟prev换
if (a[cur] < a[keyi] && ++prev!=cur)
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
void QuickSort(int\* a, int left, int right)
{
if (left >= right)
return;
int pivot=PartSort3(a, left, right);
//排完了一趟后分成了两个区域 [left,pivot] pivot [pivot+1,right];
QuickSort(a, left, pivot - 1);//递归左子区间
QuickSort(a, pivot+1, right);//递归右子区间
}
4.2.4快速排序优化
- 三数取中法选key
快速排序为什么还需要优化呢?
快速排序在选key的时候,最理想,效率最高的情况就是每次都能选到中间值
但是,快速排序在没有优化前,对数据有序的情况进行排序,那么它每次选key的值都在最左边或最右边,效率就会大大降低,
下面是我用十万个有序数据对快速排序优化前后测出来的数据对比(单位ms)
三数取中讲究的就是让我们每次选key的时候都能保证它大概中分区间,不至于每次都选到最大或最小。
代码实现:
//三数取中
int GetMidIndex(int\* a, int left, int right)
{
int mid = left + (right - left) / 2;
if (a[left] > a[mid])
{
if (a[mid] > a[right])
return mid;
else if (a[right] > a[left])
return left;
else
return right;
}
else//a[left] <= a[mid]
{
if (a[left] > a[right])
return left;
else if (a[right] > a[mid])
return mid;
else
return right;
}
}
用三数取中优化后的各个版本的快排
// 快速排序hoare版本
int PartSort1(int\* a, int left, int right)
{
int mid = GetMidIndex(a, left, right);//取相对中间的值
Swap(&a[mid], &a[left]);//跟第一个值进行交换,下面逻辑就不用变化太大,下面版本也类似
int keyi = left;
while (left < right)
{
//找小
while (left < right && a[right] >= a[keyi])
{
right--;
}
//找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
if (left < right)
{
//交换
Swap(&a[left], &a[right]);
}
}
Swap(&a[left], &a[keyi]);
return left;
}
// 快速排序挖坑法
int PartSort2(int\* a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
Swap(&a[mid], &a[left]);
int key = a[left];//保存基准值
int hole = left;//坑位
while (left < right)
{
//找小
while (left < right && a[right] >= key)
{
right--;
}
//放坑位
if (left < right)
{
a[hole] = a[right];
hole = right;//更新坑位
}
//找大
while (left < right && a[left] <= key)
{
left++;
}
if (left < right)
{
a[hole] = a[left];
hole = left;
}
}
a[hole] = key;
return hole;
}
// 快速排序前后指针法
int PartSort3(int\* a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
Swap(&a[mid], &a[left]);
int keyi = left;
int prev = left;
int cur = left+1;
while (cur <= right)
{
//cur从左往右找小跟prev换
if (a[cur] < a[keyi] && ++prev!=cur)
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
void QuickSort(int\* a, int left, int right)
{
if (left >= right)
return;
int pivot=PartSort2(a, left, right);
//分成了两个区域 [left,pivot] pivot [pivot+1,right];
QuickSort(a, left, pivot - 1);
QuickSort(a, pivot+1, right);
}
- 递归到小的子区间时,可以考虑使用插入排序
其实快速排序经过上面的三数优化之后,大体上已经没有很多问题,也不会出现最坏的情况,但是还可以进行优化
.
因为快排在实现排序的时候,当区间已经变小了,那么它的数据就相对已经有序了很多,此时我们就可以使用插入排序对它的小区间进行排序,插入排序对相对有序的数据进行排序效率是比较高的。
void QuickSort(int\* a, int left, int right)
{
if (left >= right)
return;
int pivot=PartSort2(a, left, right);
//分成了两个区域 [left,pivot] pivot [pivot+1,right];
//小区间优化
if (right - left < 8)//当区间比较小的时候就可以使用插入排序
{
InsertSort(a + left, right - left + 1);
}
else
{
QuickSort(a, left, pivot - 1);
QuickSort(a, pivot + 1, right);
}
}
4.2.5快速排序非递归
递归改循环,有两种情况
- 比较简单的递归(如斐波那契数列),可以直接改循环
- 比较复杂的递归(如快排),需要借助其他工具模拟实现
快速排序的非递归需要借助数据结构的栈去模拟递归的实现
// 快速排序 非递归实现
void QuickSortNonR(int\* a, int left, int right)
{
Stack st;
StackInit(&st);
StackPush(&st, right);
StackPush(&st, left);
while (!StackEmpty(&st))
{
int begin = StackTop(&st);
StackPop(&st);
int end = StackTop(&st);
StackPop(&st);
int pivot = PartSort2(a, begin, end);
//[begin, pivot-1] pivot [pivot+1,end]
//当区间有数才入栈
if (end - pivot > 1)
{
StackPush(&st, end);
StackPush(&st, pivot + 1);
}
if (pivot - begin > 1)
{
StackPush(&st, pivot - 1);
StackPush(&st, begin);
}
}
}
//小区间优化版本
void QuickSortNonR(int\* a, int left, int right)
{
Stack st;
StackInit(&st);
StackPush(&st, right);
StackPush(&st, left);
while (!StackEmpty(&st))
{
int begin = StackTop(&st);
StackPop(&st);
int end = StackTop(&st);
StackPop(&st);
//小区间优化
if (end - begin < 8)
{
InsertSort(a + begin, end - begin + 1);
continue;
}
int pivot = PartSort2(a, begin, end);
//分出的区间:[begin, pivot-1] pivot [pivot+1,end]
//当区间有数才入栈
if (end - pivot > 1)
{
StackPush(&st, end);
StackPush(&st, pivot + 1);
}
if (pivot - begin > 1)
{
StackPush(&st, pivot - 1);
StackPush(&st, begin);
}
}
}
可以得出: 递归和非递归的效率差不多
非递归的好处:
- 避免栈溢出
快速排序的特性总结:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
5.✨归并排序
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤
5.1递归版本
void \_MergeSort(int\* a, int begin, int end, int\* tmp)
{
if (begin >= end)
return;
//取中间数
int mid = begin + (end - begin) / 2;
//递归子问题
\_MergeSort(a, begin, mid, tmp);
\_MergeSort(a, mid + 1, end, tmp);
//合并
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
//取小的先放
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 + begin, tmp + begin, (end - begin + 1) \* sizeof(int));
}
// 归并排序递归实现
void MergeSort(int\* a, int n)
{
//开辟辅助空间
int\* tmp = (int\*)malloc(sizeof(int) \* n);
if (tmp == NULL)
{
perror("malloc fail\n");
return;
}
\_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
5.2 非递归版本
归并排序的非递归可以直接改循环处理
但是在边界处理的时候需要及其小心
// 归并排序非递归实现
void MergeSortNonR(int\* a, int n)
{
int\* tmp = (int\*)malloc(sizeof(int) \* n);
if (tmp == NULL)
{
perror("malloc fail\n");
return;
}
//gap控制每组有多少个元素进行归并
int gap = 1;
while (gap < n)
{
for (int j = 0; j < n; j += 2 \* gap)
{
//合并
int begin1 = j, end1 = j+gap-1;
int begin2 = j+gap, end2 = j+2\*gap-1;
int i = j;
//end1越界
if (end1 >= n)
{
break;
}
//begin2 这组全部越界
if (begin2 >= n)
{
break;
}
//end2越界
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, (end2 - j + 1) \* sizeof(int));
}
gap \*= 2;
}
}
归并排序的特性总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
6.✨非比较排序
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
- 统计相同元素出现次数
- 根据统计的结果将序列回收到原来的序列中
// 计数排序
void CountSort(int\* a, int n)
{
int max, min;
max = min = a[0];
//找出最大最小值
for (int i = 0; i < n; ++i)
{
if (max < a[i])
max = a[i];
if (min > a[i])
min = a[i];
}
//开辟对应大小的空间
int\* count = (int\*)malloc(sizeof(int) \* (max - min + 1));
if (count == NULL)
{
perror("malloc fail\n");
return;
}
//将辅助数组中的值初始化成0
memset(count, 0, sizeof(int) \* (max - min + 1));
//统计每个数出现的次数
for (int i = 0; i < n; ++i)
{
![img](https://img-blog.csdnimg.cn/img_convert/5a95ddd8304333ced6915f390790bab3.png)
![img](https://img-blog.csdnimg.cn/img_convert/1b79f3dc244a9d124464a3520222de01.png)
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
nter)
// 计数排序
void CountSort(int* a, int n)
{
int max, min;
max = min = a[0];
//找出最大最小值
for (int i = 0; i < n; ++i)
{
if (max < a[i])
max = a[i];
if (min > a[i])
min = a[i];
}
//开辟对应大小的空间
int* count = (int*)malloc(sizeof(int) * (max - min + 1));
if (count == NULL)
{
perror(“malloc fail\n”);
return;
}
//将辅助数组中的值初始化成0
memset(count, 0, sizeof(int) * (max - min + 1));
//统计每个数出现的次数
for (int i = 0; i < n; ++i)
{
[外链图片转存中…(img-9z3b367X-1715771583702)]
[外链图片转存中…(img-JGdHoPTH-1715771583702)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!