在前面:
本文是个人在学习数据结构整理的一些笔记.
参考过的原文博客链接我会放在文章末尾.
笔记的内容仅供参考,帮助大家学习.
1.排序的基本概念
1.1 什么是排序
1.将各个元素按照关键字递增/递减的顺序重新排列
2.排序是一种对数据集合进行整理和排列的过程,
3.通常按照一定的顺序(如升序或降序)将数据集合中的元素进行排列。
1.2 排序算法的评价指标
时间复杂度
-
一个语句的频度是指该语句在算法中被重复执行的次数。
-
而算法中的语句频度之和表示了算法问题规模的函数
-
时间复杂度就是分析频度之和的数量级
-
简单来讲:就是将该语句在算法中基本运算的执行次数的数量级作为该算法的时间复杂度。
空间复杂度
-
是指程序运行过程中所消耗的存储空间的大小,
-
通常以O(f(n))的形式来描述,其中f(n)为关于输入规模的函数。
-
一个程序在执行时除需要存储空间来存放本身所用的指令、常数、变量和输入数据外。还需要一些对数据进行操作的工作单元和存储一些为实现计算机所需信息的辅助空间。
-
如果算法的空间复杂度为O(1),表示执行该算法所需的辅助空间大小相比输入数据的规模来说是一个常量。而不表示该算法执行时不需要任何的空间或辅助空间
稳定性
-
关键字相同的元素经过排序后的相对次序是否会改变
1.3 排序的分类
内部排序
-
数据都在内存中
-
关注如何使算法的:时空间复杂度更低
-
-
指在排序期间数据对象全部存放在内存的排序
外部排序
-
数据太多,无法全部放入内存
-
还要关注如何使读写磁盘的次数更少
-
-
指在排序期间全部对象太多,不能同时存放在内存中,必须根据排序过程的要求,不断在内,外存间移动的排序。
2.选择排序
2.1 选择排序的基本思想是什么?
* 在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置,然后再从剩余未排序元素中继续寻找最小(或最大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
2.2 选择排序的核心思路是什么?
* 将排序序列分为有序区和无序区,每一趟排序从无序区中选出最小的元素放在有序区的最后,从而扩大有序区,直到全部元素有序为止。
2.3 选择排序的时间复杂度和空间复杂度是多少?该排序算法是否稳定?
-
选择排序的时间复杂度是O(n^2),其中n是数组的长度。这是因为每次选择都会遍历整个数组。
-
最好情况:O(n^2)。即使输入数据已经排好序,选择排序仍然需要比较n(n-1)/2次并移动n次数据。
-
平均情况:O(n^2)。无论输入数据是什么,选择排序都需要进行n(n-1)/2次比较。
-
最坏情况:O(n^2)。最坏的情况就是输入数据为逆序,但仍然需要n(n-1)/2次比较和n次数据移动。
-
稳定性:选择排序是不稳定的排序算法。例如,如果有两个相等的元素,一个在另一个的前面,选择排序可能会在查找最小值的过程中交换这两个元素的位置,从而改变它们的相对顺序
2.4 选择排序有哪些优点和缺点?适用于哪些场景?
-
优点:
-
1.实现简单且空间复杂度低,适合处理小型数据集
-
2.选择排序是一种原地排序算法,它只需要常数级别的额外空间(O(1))
-
3.容易理解和实现。它不需要复杂的数据结构或算法技巧,因此适合作为学习排序算法的基础
-
-
缺点:
-
1.效率较低,选择排序的时间复杂度为O(n^2),其中n是待排序序列的长度。这意味着在处理大规模数据集时,选择排序可能会变得非常慢。
-
2.不稳定:选择排序是一种不稳定的排序算法。如果在待排序的序列中存在两个或两个以上相等的元素,选择排序可能会改变它们的相对位置
-
-
适用场景
-
小型数据集
-
稳定性要求不高
-
内存受限
-
2.5 演示效果
2.6 代码
核心代码:
#include<iostream>
//交换
void swap(int* x, int* y) {
int temp = *y;
*y = *x;
*x = temp;
}
//简单选择排序
void selectSort(int A[],int length) {
int i, j;
for (i = 0;i < length-1;i++) { //比较length-1次即可
int min = i; //记录最小元素的位置
for (j = i + 1;j < length;j++) {
if (A[j] < A[min]) min = j; //更新最小元素位置
}
if (min != i) swap(&A[i], &A[min]); //排完一轮就一次交换元素
}
}
全部代码:
#include<iostream>
//交换
void swap(int* x, int* y) {
int temp = *y;
*y = *x;
*x = temp;
}
//简单选择排序
void selectSort(int A[],int length) {
int i, j;
for (i = 0;i < length-1;i++) { //比较length-1次即可
int min = i; //记录最小元素的位置
for (j = i + 1;j < length;j++) {
if (A[j] < A[min]) min = j; //更新最小元素位置
}
if (min != i) swap(&A[i], &A[min]); //排完一轮就一次交换元素
}
}
void PrintFs(int A[],int length) {
for (int i = 0;i < length;i++) {
std::cout << A[i] << " ";
}
std::cout << std::endl;
}
int main() {
int Arr[] = { 6,5,1,2,8,9,10,47,54 };
int num = 9;
selectSort(Arr, num);
PrintFs(Arr, num);
return 0;
}
2.7 总结
3.插入排序
3.1 插入排序的基本思想是什么?
通过构建新的有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入
3.2 插入排序的核心思路是什么?
-
1.从第一个元素开始,该元素可以认为已经被排序
-
2.取出下一个元素,在已经排序的元素序列中从后向前扫描
-
3.如果该元素(已排序)大于新元素,将该元素移到下一位置
-
4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
-
5.将新元素插入到该位置后
-
6.重复步骤2~5
-
每次迭代都把未排序序列中的最小(或最大)元素移到合适的位置,直到所有元素都排序完毕
3.3 插入排序的时间复杂度和空间复杂度是多少?该排序算法是否稳定?
-
时间复杂度
-
插入排序的时间复杂度是 O(n^2),其中 n 是待排序元素的数量。
-
这是因为它涉及到两层嵌套循环,外层循环用于遍历整个数组,内层循环用于在已排序的元素中查找插入位置。
-
最好情况:原本有序O(n)
-
最坏情况:原本逆序O(n^2)
-
平均情况:O(n^2)
-
-
空间复杂度
-
插入排序的空间复杂度是O(1),因为它是一种原地排序算法,只需要常量的额外空间。
-
-
稳定性
-
稳定排序算法是指如果存在两个相等的元素,排序前后它们的相对位置不变。
-
插入排序是一种稳定排序算法
-
3.4 插入排序有哪些优点和缺点?适用于哪些场景?
-
优点
-
实现简单,理解起来比较容易。
-
对小规模数据或部分有序的数据排序效果好,时间复杂度可以达到O(n)。
-
插入排序是稳定的排序算法。
-
适用于链表的排序。
-
空间复杂度低,为O(1)
-
-
缺点
-
对于大规模数据,插入排序的效率较低,时间复杂度较高,最坏和平均情况下都是O(n^2)。
-
插入排序在每次插入时都需要移动元素,这增加了不必要的操作,影响效率。
-
-
适用场景
-
1.数据量较小的情况下
-
由于插入排序在数据量较小的情况下效率较高,因此对于小规模数据的排序,插入排序是一个不错的选择。
-
-
2.数据基本有序的情况
-
当数据已经基本有序时,插入排序的性能会非常好,时间复杂度接近O(n)
-
-
3.链表排序
-
由于插入排序是通过比较和移动元素来进行排序的,因此它非常适合用于链表的排序。
-
-
3.5 演示效果
3.6 代码
-
插入排序-升序
#include <iostream>
// 插入排序-升序
// arr: 待排序的数组
// n: 数组中的元素个数
void InsertSortBy(int arr[], int n) {
int i, j, temp;
// 遍历从数组的第二个元素开始,因为第一个元素默认已经是“有序”的
for (i = 1; i < n; ++i) { // 假设第一个已经排好了,从第二个开始。
// 保留当前遍历到的元素,准备进行插入
temp = arr[i]; // 将i的指针指向的元素保留在temp中
// 从当前元素的前一个元素开始向前遍历,直到找到应该插入的位置
for (j = i - 1; arr[j] > temp && j >= 0; --j) { // 每一轮开始:j指针指向i的前面一位。依次用arr[j]比较temp的大小
// 如果当前元素比要插入的元素大,则向后移动一位
arr[j + 1] = arr[j]; // j指向的元素位置后移。
}
// 找到插入位置后,将temp插入到正确的位置
arr[j + 1] = temp; // 复制到插入位置 注意这里是先-- 才不满足的for条件。所以要加1.插入到后面
}
}
// 数组显示函数
// brr: 要显示的数组
// n: 数组中的元素个数
void Display(int brr[], int n) {
for (int i = 0; i < n; ++i) {
std::cout << brr[i] << " ";
}
std::cout << std::endl;
}
int main() {
// 初始化待排序数组
int krr[8] = {49, 38, 65, 97, 76, 13, 27, 49};
int n = 8; // 数组中的元素个数
// 调用插入排序函数
InsertSortBy(krr, n);
// 调用数组显示函数
Display(krr, n);
return 0;
}
-
插入排序-带哨兵-升序
#include<iostream>
// 插入排序-带哨兵-升序-(特点:数组A[0]不放元素,只放哨兵)
void InsertSortBySentinel(int A[], int n) {
if (n <= 1) return; // 如果元素数量小于等于1,则无需排序
int i, j;
for (i = 1; i < n; i++) { // 从第一个有效元素开始
if (A[i] < A[i-1]) {
A[0] = A[i]; // A[0]用作哨兵,暂存待插入元素
for (j = i - 1; j > 0 && A[0] < A[j]; --j) { // 检查并移动大于A[0]的元素
A[j + 1] = A[j];
}
A[j + 1] = A[0]; // 插入A[0](即待插入元素)到正确位置
}
}
}
void DisPlaySentinel(int A[], int n) {
for (int i = 1; i < n; ++i) { // 从第一个有效元素开始显示
std::cout << A[i] << " ";
}
std::cout << std::endl;
}
int main() {
int n = 8; // 8是有效元素数量
int jrr[9] = { 0, 49, 38, 65, 97, 76, 13, 27, 49 }; // 0处作为哨兵
InsertSortBySentinel(jrr, n+1); // 传入元素数量n+1
DisPlaySentinel(jrr, n + 1); // 显示时包括哨兵位置,所以为n+1
return 0;
}
-
优化-折半插入排序
#include<iostream>
// 折半插入排序-带哨兵-升序
// 特点:数组A[0]不放元素,只放哨兵
void InsertSortByMidSentinel(int A[], int n) {
int i, j, low, high, mid;
// 从数组的第二个有效元素开始遍历
for (i = 1; i < n; i++) { // 注意这里应该从1开始,因为数组从1开始存储有效数据
if (A[i] < A[i - 1]) {
A[0] = A[i]; // 将待插入的元素保存到哨兵位置
low = 1; high = i - 1; // 设置折半查找的范围
while (low <= high) {
mid = (low + high) / 2; // 取中间点,这里可能产生整数溢出,应使用整除运算符
if (A[mid] > A[0]) {
high = mid - 1; // 查找左子表
} else {
low = mid + 1; // 查找右子表,注意这里缺少一个else分支的左大括号
}
}
// 将比A[0]大的元素后移
for (j = i - 1; j >= low; --j) { // 注意这里应该是j >= low,否则可能错过插入位置
A[j + 1] = A[j];
}
A[low] = A[0]; // 插入A[0](即待插入元素)到正确位置,注意这里应该是low
}
}
}
// 显示数组(不包括哨兵位置)
void DisPlaySentinel(int A[], int n) {
for (int i = 1; i < n; ++i) {
std::cout << A[i] << " ";
}
std::cout << std::endl;
}
int main() {
int n = 8; // 有效元素为8个
int jrr[9] = { 0, 49, 38, 65, 97, 76, 13, 27, 49 }; // 0 处作为哨兵
InsertSortByMidSentinel(jrr, n + 1); // 传入n+1是因为数组大小是n+1,但排序时只考虑前n个元素
DisPlaySentinel(jrr, n + 1); // 显示时包括哨兵位置,但实际上不打印哨兵
return 0;
}
3.7 总结
4.希尔排序
4.1 希尔排序的基本思想是什么?
-
希尔排序是插入排序的一种更高效的改进版本,也称为 ’‘缩小增量排序’‘
-
它的基本思想是将待排序的数组按下表的一定增量分组,对每组采用直接插入排序算法。然后将增量缩小,继续分组排序,当增量减至1时,排序完正好有序
4.2 希尔排序的核心思路是什么?
-
希尔排序的核心思路是分组插入排序,随着增量h的逐渐减小,每次分组排序后,整个序列有序性会不断增加,直到完全有序
-
1.选取一个整数h(称为增量),将待排序序列分为若干个长度为h的子序列
-
2.分别对它们进行直接插入排序
-
3.依次缩小增量h,重复上述分组和排序过程,直到增量h为1为止。
4.3 希尔排序的时间复杂度和空间复杂度是多少?该排序算法是否稳定?
-
时间复杂度
-
希尔排序的时间复杂度与间隔序列的选取有关
-
希尔排序的时间复杂度依赖于间隔序列的选择。对于某些间隔序列,希尔排序的时间复杂度可以达到O(n^1.3),但在最坏的情况下,如果间隔序列选择不当,希尔排序的时间复杂度可能会退化为O(n^2)
-
在实际应用中,希尔排序的性能通常优于O(n^2),接近于O(n log n)
-
-
空间复杂度
-
希尔排序的空间复杂度是O(1),因为它只需要一个额外的空间来存储临时变量,不需要额外的存储空间来存储待排序的元素
-
-
稳定性
-
希尔排序是不稳定的排序算法
-
因为在分割子序列进行插入排序时,可能会改变相等元素的相对顺序
-
当两个相等的元素分别位于不同的子序列中时,它们可能会被独立地插入排序,导致它们的相对顺序被改变。如果需要保持相等元素的相对顺序(即稳定排序),则不能使用希尔排序
-
4.4 希尔排序的优点和缺点是什么?适用于哪些场景?
-
优点
-
速度较快
-
希尔排序是插入排序的一种优化版本,它在处理大型数组时,比简单的插入排序要快得多
-
因为希尔排序在开始时使用较大的间隔,将数组分割成多个子数组,对每个子数组进行插入排序,这样可以使得整个数组的元素更加接近有序,从而减少后续插入排序的交换和移动次数
-
-
适应性强
-
无论数据是随机分布的还是已经部分有序的,希尔排序都能给出较好的性能。
-
-
易于实现
-
希尔排序的实现相对简单,代码量也较少
-
-
-
缺点
-
不稳定性
-
待排序的数组中存在相等的元素,那么这些元素的相对顺序可能会被改变
-
-
时间复杂度分析困难
-
希尔排序的时间复杂度与间隔序列的选取有关,分析时间复杂度没有一个统一的标准
-
-
对于大规模数据的排序效率不高
-
当需要处理的数据量非常大时,希尔排序可能不是最优的选择,因为它的时间复杂度仍然高于快速排序、归并排序等更高效的排序算法
-
-
-
适用场景
-
中等规模数据的排序
-
部分有序数据的排序
-
内存限制严格的环境
-
4.5 演示效果
4.6 代码
- 减半策略
#include <iostream>
// 希尔排序函数
// A: 要排序的数组
// n: 数组的大小
void ShellSort(int A[], int n) {
int d, i, j;
// 从数组长度的一半开始作为间隔,逐渐减半,直到间隔为1
for (d = n / 2; d >= 1; d /= 2) { // 使用更简单的增量序列(这里是减半策略)
// 从间隔d开始遍历数组
for (i = d; i < n; ++i) {
// 将当前元素暂存到temp中
int temp = A[i];
// 从当前位置向前比较,间隔为d
for (j = i; j >= d && A[j - d] > temp; j -= d) {
// 如果前一个间隔d的元素比temp大,则将其后移
A[j] = A[j - d];
}
// 找到合适的位置后,将temp插入
A[j] = temp;
}
}
}
// 打印函数
// A: 要打印的数组
// n: 数组的大小
void DisPlay(int A[], int n) {
for (int i = 0; i < n; ++i) {
std::cout << A[i] << " ";
}
std::cout << std::endl;
}
int main() {
int n = 8;
int jrr[8] = {49, 38, 65, 97, 76, 13, 27, 49}; // 初始化一个数组
ShellSort(jrr, n); // 对数组进行希尔排序
DisPlay(jrr, n); // 打印排序后的数组
return 0;
}
- 除3法
void ShellSortByName(int A[], int n) {
int d = n;
while (d > 1) {
d = d / 3 + 1; // 调整增量
for (int i = 0; i < n - d; i++) {
int j = i;
int temp = A[j + d];
for (; j >= 0 && A[j] > temp; j -= d) {
A[j + d] = A[j];
}
A[j + d] = temp;
}
}
}
4.7 总结
5. 冒泡排序
5.1 冒泡排序的基本思想是什么?
-
冒泡排序的基本思想是通过相邻元素之间的比较和交换,使得每一趟排序都能将当前未排序部分的最大(或最小)元素交换到其最终相应处的位置,直到整个序列有序.
5.2 冒泡排序的核心思路是什么?
-
1.比较和交换
-
从序列的一端开始,比较相邻的两个元素,(按排序规则,进行比较),如果它们的顺序不对,则进行交换.
-
-
2.重复遍历
-
从序列的一端到另一端进行上述比较和交换操作-- 称为一趟排序.
-
一趟排序后,最大的元素(或最小的元素,取决于排序的方式)会被"冒泡"到序列的一端.
-
-
3.多趟排序
-
对除了已排好序的一端之外的所有元素重复上述操作,直到整个序列有序.
-
5.3 冒泡排序的时间复杂度和空间复杂度是多少?该排序算法是否稳定?
-
时间复杂度
-
最好情况(输入数组已经有序)
-
在这种情况下,冒泡排序只需要进行一次遍历即可确定数组已经有序,因此时间复杂度为O(n),其中n是数组的长度.
-
-
最坏情况(输入数组完全逆序)
-
在这种情况下,冒泡排序需要进行n-1趟排序,每趟排序都需要进行n-i次比较和交换操作(i是当前趟数),因此总的时间复杂度为O(n^2).
-
-
平均情况
-
冒泡排序的时间复杂度是O(n^2).
-
-
-
空间复杂度
-
冒泡排序是一种原地排序算法.
-
只需要一个额外的空间来存储临时变量(用于交换元素),因此空间复杂度为O(1).
-
-
稳定性
-
当两个相邻的元素大小相等时,它们不会进行交换,因此原本相等的元素在排序后的相对位置不会发生变化.
-
冒泡排序是稳定的排序算法.
-
5.4 冒泡排序的优点和缺点是什么?适用于哪些场景?
-
优点
-
简单易懂
-
冒泡排序是一种非常直观的排序算法,对于初学者来说很容易理解.
-
-
原地排序
-
冒泡排序只需要一个额外的空间来存储临时变量(用于交换元素),因此它可以在原地对数组进行排序,空间复杂度为O(1).
-
-
稳定排序
-
冒泡排序是稳定的排序算法.
-
-
-
缺点
-
时间效率低
-
冒泡排序的时间复杂度为O(n^2),在数据量大的时候效率很低.
-
-
额外空间需求
-
冒泡排序本身不需要额外的存储空间,但如果需要记录排序的过程(如使用“标志位”优化算法时),则可能需要额外的存储空间.
-
-
对大数据集不友好
-
对于大规模的数据集,冒泡排序的效率非常低.
-
-
-
适用场景
-
小规模数据集.
-
稳定性要求高的场景.
-
5.5 演示效果
-
左边大于右边交换一趟排下来最大的在右边 (升序)
5.6 代码
-
外部循环只需要比较n-1趟。
-
内部循环只需要比较n-1-i趟。(其中i为已经排好序的个数)
// 交换两个整数的函数
void swap(int *x, int *y) {
int temp = *y; // 将y的值保存到临时变量temp中
*y = *x; // 将x的值赋给y
*x = temp; // 将temp(原来的y的值)赋给x
}
// 冒泡排序-升序
void BubbleSort(int A[], int n) {
int p, i;
bool flag; // 用于标记一趟排序中是否发生过交换
for (p = n - 1; p >= 0; p--) { // 从数组的末尾开始向前遍历,总共需要比较n-1趟
flag = false; // 每趟开始前假设没有发生过交换
for (i = 0; i < p; i++) { // 遍历到倒数第二个元素即可,因为最后一个元素已经是当前最大(或最小)
if (A[i] > A[i + 1]) { // 如果当前元素大于下一个元素,则交换它们的位置
swap(&A[i], &A[i + 1]); // 调用swap函数交换A[i]和A[i+1]
flag = true; // 标记发生了交换
}
}
if (flag == false) break; // 如果一趟遍历下来都没有发生交换,则数组已经有序,可以提前结束排序
}
}
// 冒泡排序-降序
void BubbleSortOrderBy(int A[], int n) {
int p, i;
bool flag; // 用于标记一趟排序中是否发生过交换
for (p = n - 1; p >= 0; p--) { // 从数组的末尾开始向前遍历,总共需要比较n-1趟
flag = false; // 每趟开始前假设没有发生过交换
for (i = 0; i < p; i++) { // 遍历到倒数第二个元素即可,因为最后一个元素已经是当前最大(或最小)
if (A[i] < A[i + 1]) { // 如果当前元素小于下一个元素,则交换它们的位置(降序排序)
swap(&A[i], &A[i + 1]); // 调用swap函数交换A[i]和A[i+1]
flag = true; // 标记发生了交换
}
}
if (flag == false) break; // 如果一趟遍历下来都没有发生交换,则数组已经有序,可以提前结束排序
}
}
5.7 总结
6. 快速排序
6.1 快速排序的基本原理
-
快速排序是一种分而治之的排序算法,采用了一种“分区”的策略
-
将待排序的数组划分为两部分,其中一部分的所有元素都比另一部分的所有元素小(或大)
-
然后再对这两部分递归地应用同样的分区过程,从而达到整个数组的排序。
6.2 快速排序的核心思路
-
1.选择一个基准元素(pivot),通常选择数组的第一个或最后一个元素。
-
2.通过一趟排序将待排序的记录分隔成独立的两部分,其中一部分的所有记录的关键字均比另一部分的所有记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
-
3.递归地将上述过程应用于这两部分,直到每部分只有一个元素或空为止。
-
具体步骤(一趟排序)
-
1、选出一个key值(一般key是最左边值或最右边值)
-
2、定义一个left指针和right指针。left只向右走,right只向左走。
-
3、选取key为最左边值(right先走)【或者选取key为最右边值(left先走)】
-
4.若left<right
-
4.1、right指针指向的值和key进行比较。【若right>=key,right左移一位,重复步骤4.1】
-
↓
-
4.2、left指针指向的值和key进行比较。【若key>=left,left右移一位,重复步骤4.2】
-
↓
-
4.3 交换left和right值
-
-
5.交换key和left值(或者right值)
-
6.3 演示效果
6.4 代码实现
- 核心代码
// 划分区域
int Partition(int A[], int left, int right) {
int key = A[left];
while (left < right) {
while (left < right && A[right] >= key) --right;
A[left] = A[right];
while (left < right && A[left] <= key) ++left;
A[right] = A[left];
}
A[left] = key;
return left;
}
// 快速排序
void QuickSort(int A[], int left, int right) {
if (left < right) {
int pivotpos = Partition(A, left, right);
QuickSort(A, left, pivotpos - 1);
QuickSort(A, pivotpos + 1, right);
}
}
- 全部代码
#include<iostream> // 引入标准输入输出流库,用于打印输出
// 划分区
int Partition(int A[], int left, int right) {
int pivot = A[left]; // 选取基准值,这里选择left位置的元素作为基准
while (left < right) { // 当left小于right时循环,确保数组被划分
while (left < right && A[right] >= pivot) { // high指针向左移动,直到找到一个比pivot小的元素
--right;
}
A[left] = A[right]; // 将找到的较小元素放到low位置
while (left < right && A[left] <= pivot) { // left指针向右移动,直到找到一个比pivot大的元素
++left;
}
A[right] = A[left]; // 将找到的较大元素放到right位置
}
A[left] = pivot; // 最后将基准值放到正确的位置
return left; // 返回基准值的最终位置,这也是划分后左右两部分的分界点
}
// 快速排序
void QuickSort(int A[], int left, int right) {
if (left < right) { // 如果left小于right,说明数组还有至少两个元素,可以进行划分
int pivotpos = Partition(A, left, right); // 调用Partition函数进行划分,得到基准值的位置
QuickSort(A, left, pivotpos - 1); // 递归对基准值左边的子数组进行排序
QuickSort(A, pivotpos + 1, right); // 递归对基准值右边的子数组进行排序
}
}
// 打印函数
void DisPlay(int A[], int n) {
for (int i = 0; i < n; ++i) { // 遍历数组
std::cout << A[i] << " "; // 打印当前元素,后面跟一个空格
}
std::cout << std::endl; // 打印换行符,结束当前行的输出
}
int main() {
int jrr[8] = { 49, 38, 65, 97, 76, 13, 27, 49 }; // 定义并初始化一个整型数组
QuickSort(jrr, 0, 7); // 调用快速排序函数,对数组进行排序,注意数组索引是从0开始的,所以high是7
DisPlay(jrr, 8); // 调用打印函数,打印排序后的数组,数组元素个数是8
return 0; // 主函数返回0,表示程序正常结束
}
6.5 快速排序的时间复杂度和空间复杂度,该排序算法是否稳定?
-
时间复杂度
-
最优情况:O(n log n)。每次划分都能将数组分成两个大致相等的部分。
-
最坏情况:O(n^2)。当输入的数组已经有序或逆序时,快速排序会退化为冒泡排序,导致时间复杂度达到最高。
-
平均情况:O(n log n)。在实际应用中,通过随机选择基准元素或采用“三数取中”等策略,可以使得划分尽量均衡,从而接近最优情况。
-
-
空间复杂度
-
最优情况:O(log n)。这是递归调用栈所需的空间,当每次划分都相对均衡时,递归树的深度为log n。
-
最坏情况:O(n)。当输入的数组已经有序或逆序时,递归树将退化为链表,导致需要n个栈空间。
-
平均情况:O(log n)。与最优情况相同,通过合适的策略可以使划分尽量均衡。
-
-
稳定性
-
快速排序是不稳定的排序算法
-
6.6 快速排序的优点和缺点是什么?适用于那些场景?
-
优点
-
平均效率高
-
在平均情况下,快速排序的时间复杂度为O(n log n)
-
-
原地排序
-
快速排序是原地排序算法,只需要O(log n)的额外空间(用于递归调用栈),因此空间效率较高
-
-
适用于大数据集
-
由于快速排序采用了分而治之的策略,因此它可以很好地处理大数据集
-
-
-
缺点
-
最坏情况性能差
-
当输入的数组已经有序或逆序时,快速排序会退化为O(n^2)的时间复杂度
-
-
稳定性差
-
快速排序是不稳定的排序算法。相等的元素在排序后的相对位置可能会发生改变
-
-
递归调用栈开销
-
然快速排序的空间复杂度是O(log n),但在最坏情况下,递归调用栈可能会占用较多的空间。
-
-
-
适用场景
-
大数据集
-
内存限制严格
-
不需要稳定排序
-
6.7 算法效率分析
6.8 总结
参考链接:
六大排序算法:插入排序、希尔排序、选择排序、冒泡排序、堆排序、快速排序https://blog.csdn.net/weixin_50886514/article/details/119045154?
【排序算法】希尔排序(C语言)https://blog.csdn.net/weixin_52811588/article/details/126454328?
-----后续会持续更新...