网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
排序是处理数据的一种最常见的操作,所谓排序就是将数据按某字段规律排列,所谓的字段就是数据节点的其中一个属性。比如一个班级的学生,其字段就有学号、姓名、班级、分数等等,我们既可以针对学号排序,也可以针对分数排序。
- 稳定性
在一组无序数据中,若两个待排序字段一致的数据,在排序前后相对位置不变,则称排序算法是稳定的,否则是不稳定的。 - 内排序与外排序
如果待排序数据量不大,可以一次性全部装进内存进行处理,则称为内排序,若数据量大到无法一次性全部装进内存,而需要将数据暂存外存,分批次读入内存进行处理,则称为外排序。
不同的排序算法性能也不同,这句话什么意思呢?就比如有五个保洁阿姨(算法),你派这五个阿姨去打扫房间,每个阿姨需要打扫完房间的时间肯定是不一样的,有些阿姨要20分钟,有些要10分钟,有些要15分钟,这就叫性能。但可能打扫快的阿姨的房间可能不是很干净,那怎么挑选打扫时间又快又干净的阿姨呢?详细性能数据如下表所示。
从表中可以得到一些简单的指导思路:
- 选择排序、插入排序和冒泡排序思路简单,但时间效率较差,只适用于数据样本较小的场合,这几种算法的好处是不需要额外开辟空间,空间复杂度是常量。
- 希尔排序是插入排序的改进版,在平均情况下时间效率要比直接插入法好很多,也不需要额外开辟空间,要注意的是希尔排序是不稳定排序。
- 快速排序是所有排序算法中时间效率最高的,但由于快排是一种递归运算,对内存空间要求较高,当数据量较大时,会消耗较多的内存。
看不懂这个表没关系的,我刚开始学的时候也看不懂。或者你不看这个表也没关系的,只要记住算法的思路就行了。
冒泡排序
这个排序笔试题出现的几率真的很高,实习找工作的大学生必须给我死记硬背下来。
首先引入两个概念:
- 顺序:如果两个数据的位置符合排序的需要,则称它们是顺序的。
- 逆序:如果两个数据的位置不符合排序需要,则称它们是逆序的。
冒泡排序基于这样一种简单的思路:从头到尾让每两个相邻的元素进行比较,顺序就保持位置不变,逆序就交换位置。可以预料,经过一轮比较,序列中具有“极值”的数据,将被挪至序列的末端。
以下几个步骤:
- 从数组的第一个元素开始,依次比较相邻的两个元素的大小。
- 如果前一个元素大于后一个元素,则交换它们的位置。
- 继续比较相邻的下一对元素,直到最后一个元素。
- 在第一次遍历完成后,最大的数会被放到数组的最后位置。
- 在第二次遍历时,由于最后一个元素已经确定是最大值,因此可以跳过它。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
文字可能有点难懂的话,直接看图!
示例代码:
#include <stdio.h> // 引入标准输入输出库
// 定义冒泡排序函数
void bubbleSort(int arr[], int n)
{
int i, j, temp; // 声明循环变量i, j和临时变量temp
for (i = 0; i < n - 1; i++) // 外层循环,总共需要遍历n-1次
{
for (j = 0; j < n - i - 1; j++) // 内层循环,每次比较相邻元素并交换位置
{
if (arr[j] > arr[j + 1]) // 如果前一个元素大于后一个元素
{
// 交换两个元素的位置
temp = arr[j]; // 将arr[j]的值保存到临时变量temp中
arr[j] = arr[j + 1]; // 将arr[j+1]的值赋给arr[j]
arr[j + 1] = temp; // 将临时变量temp中的值赋给arr[j+1]
}
}
}
}
int main()
{
int arr[] = {64, 34, 25, 12, 22, 11, 90}; // 定义待排序数组
int n = sizeof(arr) / sizeof(arr[0]); // 计算数组长度
// 打印原始数组
printf("原始数组:\n");
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]); // 打印数组元素
}
printf("\n"); // 打印换行符
// 调用冒泡排序函数
bubbleSort(arr, n);
// 打印排序后的数组
printf("排序后的数组:\n");
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]); // 打印数组元素
}
printf("\n"); // 打印换行符
return 0; // 程序正常退出
}
插入排序 (这个也要记住因为面试官可能会问你了解那些排序算法)
插入排序(Insertion Sort)的思路非常直观,它借鉴了我们日常生活中整理扑克牌或列表的方式。其基本思想是:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
以下是插入排序的详细思路:
- 初始化:从数组的第一个元素开始,该元素可以认为已经被排序。
- 遍历:从数组的第二个元素开始,依次遍历到最后一个元素。
- 插入:对于当前遍历到的元素,假设为
arr[i]
,我们将其视为一个待插入的元素。 - 比较与移动:从
arr[i-1]
开始向前遍历(即从当前元素的前一个元素开始),比较该元素与待插入元素arr[i]
的大小。如果arr[i-1]
大于arr[i]
,则将arr[i-1]
后移一位,继续向前比较。这个过程一直持续到找到arr[i]
应该插入的位置,或者已经遍历到数组的开始。 - 插入:找到位置后,将
arr[i]
插入到该位置。 - 重复:对数组的下一个元素重复步骤3至5,直到所有元素都被排序。
下面是一个简单的例子来展示插入排序的过程:
假设我们有以下数组:[4, 3, 2, 10, 12, 1, 5, 6]
- 初始时,第一个元素
4
是已排序的。 - 接下来,我们考虑
3
,将其与4
比较,发现3
应该放在4
前面,所以交换它们的位置,得到[3, 4, 2, 10, 12, 1, 5, 6]
。 - 接下来是
2
,依次与4
、3
比较并交换位置,得到[2, 3, 4, 10, 12, 1, 5, 6]
。 - 然后是
10
,因为它大于它前面的所有元素,所以直接放在末尾,数组变为[2, 3, 4, 10, 12, 1, 5, 6]
。 - 对于
12
,也是直接放在末尾,数组变为[2, 3, 4, 10, 1, 12, 5, 6]
。 - 对于
1
,将其与前面的元素比较并依次交换位置,直到放到正确的位置,得到[1, 2, 3, 10, 4, 12, 5, 6]
。 - 以此类推,直到整个数组排序完成。
插入排序对于小规模或部分有序的数据表现较好,时间复杂度在最好情况下为O(n),平均和最坏情况下为O(n^2)。在实际应用中,如果知道数据大部分已经是有序的,或者数据量不大,插入排序是一个简单而有效的选择。然而,对于大规模随机数据,通常选择更高效的排序算法,如快速排序、归并排序或堆排序等。
示例代码:
#include <stdio.h>
// 插入排序函数
void insertionSort(int arr[], int n)
{
int i, key, j;
for (i = 1; i < n; i++) // 从第二个元素开始遍历
{
key = arr[i]; // 将当前元素保存为key
j = i - 1; // 前一个元素的索引
while (j >= 0 && arr[j] > key)// 如果前一个元素大于key,则将前一个元素后移一位
{
arr[j + 1] = arr[j];
j = j - 1; // 向前移动一位
}
// 找到key的插入位置,将其插入
arr[j + 1] = key;
}
}
int main()
{
int arr[] = {4, 3, 2, 10, 12, 1, 5, 6}; // 待排序数组
int n = sizeof(arr) / sizeof(arr[0]); // 数组长度
// 打印原始数组
printf("原始数组:\n");
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
// 调用插入排序函数
insertionSort(arr, n);
// 打印排序后的数组
printf("排序后的数组:\n");
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
代码中,insertionSort
函数负责执行插入排序算法。对于arr
数组的每一个元素,我们从第二个元素开始(索引为1),将其作为key
,然后和它之前的元素逐一比较。如果前面的元素比key
大,我们就把前面的元素后移一位,直到找到key
的正确位置并插入。main
函数中,我们定义了待排序的数组arr
,并计算了数组的长度n
。然后,我们打印出原始数组的元素,调用insertionSort
函数进行排序,最后打印出排序后的数组元素。
选择排序
选择排序(Selection Sort)是一种简单直观的排序算法。它的工作原理是首先在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(或最大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
以下是选择排序的详细思路:
- 初始化:设置两个指针,一个指向已排序部分的末尾(通常初始化为数组的第一个元素的前一个位置),另一个指向未排序部分的开始(通常是数组的第一个元素)。
- 查找最小值:从未排序部分的第一个元素开始,遍历到未排序部分的末尾,寻找最小的元素。
- 交换:找到最小元素后,将其与未排序部分的第一个元素交换位置。这样,最小元素就被放到了已排序部分的末尾。
- 移动指针:将已排序部分的末尾指针向后移动一位,这样下一个位置就用来存放下一个最小元素。
- 重复过程:重复第2步到第4步,直到已排序部分的末尾指针到达数组的末尾,此时数组就已经完全排序好了。
需要注意的是,选择排序是不稳定的排序算法,即相等的元素在排序后可能会改变原有的相对顺序。
举个例子,假设有一个数组 [64, 25, 12, 22, 11]
,按照选择排序的思路,排序过程如下:
- 初始状态:
[64, 25, 12, 22, 11]
- 第一轮:找到最小元素11,与第一个元素64交换,得到
[11, 25, 12, 22, 64]
- 第二轮:在未排序部分
[25, 12, 22, 64]
中找到最小元素12,与第二个元素25交换,得到[11, 12, 25, 22, 64]
- 第三轮:在未排序部分
[25, 22, 64]
中找到最小元素22,与第三个元素25交换,得到[11, 12, 22, 25, 64]
- 第四轮:在未排序部分
[25, 64]
中找到最小元素25,无需交换,因为它已经在正确的位置 - 第五轮:只剩下一个元素64,无需排序
最终数组变为 [11, 12, 22, 25, 64]
,完成排序。
选择排序的时间复杂度为 O(n^2),其中 n 是待排序数组的长度。尽管它的效率不是最高的,但由于其实现简单,因此在教学和简单应用中仍然有一定的使用场景。
示例代码:
#include <stdio.h>
// 选择排序函数
void selectionSort(int arr[], int n)
{
int i, j, minIndex, temp;
// 遍历所有数组元素
for (i = 0; i < n - 1; i++)
{
// 假设当前索引位置的元素是最小的
minIndex = i;
// 在未排序部分查找最小元素
for (j = i + 1; j < n; j++)
{
// 如果发现更小的元素,更新最小元素的索引
if (arr[j] < arr[minIndex])
{
minIndex = j;
}
}
// 如果找到的最小元素不在当前位置,则交换它们
if (minIndex != i)
{
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
int main()
{
int arr[] = {64, 25, 12, 22, 11};
int n = sizeof(arr) / sizeof(arr[0]);
// 打印原始数组
printf("原始数组:\n");
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
// 调用选择排序函数
selectionSort(arr, n);
// 打印排序后的数组
printf("排序后的数组:\n");
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
快速排序
快排是一种递归思想的排序算法,先比较其他的排序算法,它需要更多内存空间,但快排的语句频度是最低的,理论上时间效率是最高的。
快速排序的基本思路是:在待排序序列中随便选取一个数据,作为所谓“支点”,然后所有其他的数据与之比较,以从小到大排序为例,那么比支点小的统统放在其左边,比支点大的统统放在其右边,全部比完之后,支点将位与两个序列的中间,这叫做一次划分(partition)。
一次划分之后,序列内部也许是无序的,但是序列与支点三者之间,形成了一种基本的有序状态,接下去使用相同的思路,递归地对左右两边的子序列进行排序,直到子序列的长度小于等于1为止。
// 黄色:支点
// 绿色:比支点小的数据
// 紫色:比支点大的数据
// 红色:用来和支点比较大小的(现在比的位置)
// 橙色:已经比较好的数据
注意:快速排序操作复杂,上述gif动图并没有完全将之展示出来
示例代码:
#include <stdio.h>
// 快速排序的函数原型声明
void quickSort(int arr[], int left, int right);
// 交换数组中两个元素的位置
void swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
// 快速排序函数
void quickSort(int arr[], int left, int right)
{
if (left < right)
{
// 分割数组,并返回pivot的位置
int pivotIndex = partition(arr, left, right);
// 对pivot左边的子数组进行递归排序
quickSort(arr, left, pivotIndex - 1);
// 对pivot右边的子数组进行递归排序
quickSort(arr, pivotIndex + 1, right);
}
}
// 分割数组的函数
int partition(int arr[], int left, int right)
{
// 选择最右侧的元素作为基准值
int pivot = arr[right];
int i = left - 1; // 指向小于基准值的元素的最后一个位置
for (int j = left; j < right; j++)
{
// 如果当前元素小于或等于基准值
if (arr[j] <= pivot)
{
// 将小于基准值的元素移动到左边
i++;
swap(&arr[i], &arr[j]);
}
}
// 将基准值放到正确的位置
swap(&arr[i + 1], &arr[right]);
return i + 1; // 返回基准值的索引位置
}
// 主函数
int main()
{
int arr[] = {10, 7, 8, 9, 1, 5};
int n = sizeof(arr) / sizeof(arr[0]);
printf("原始数组:\n");
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
// 对数组进行快速排序
quickSort(arr, 0, n - 1);
printf("排序后的数组:\n");
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
希尔排序
希尔排序(Shell Sort)是插入排序的一种更高效的改进版本,也称为缩小增量排序。它通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已经排好了的(只剩少量的数据需要插入到已排好序的序列中),所以速度很快。
希尔排序的基本思路是:
- 选择一个增量序列:增量序列的选取对希尔排序的时间性能至关重要。通常,初始增量较大,随后增量逐渐减少,直至增量为1,即最后一次增量排序后序列基本有序。
- 分组排序:根据当前的增量值,将待排序的序列分割成若干长度为m的子序列(m为当前增量),然后对每个子序列进行直接插入排序。
- 减小增量:每次排序完成后,将增量按照一定的规则减小,通常是除以2或其他小于1的正数。
- 重复分组排序:使用新的增量值重复分组排序的步骤,直到增量减小到1。当增量为1时,整个序列已经基本有序,这时进行一次普通的插入排序,即可得到完全有序的序列。
希尔排序在开始时增量较大,这样它可以让元素移动更远,从而更快地使整个序列接近有序。随着增量的逐渐减小,排序的粒度也越来越细,直到增量为1时,完成最后的微调。
希尔排序的时间复杂度依赖于增量序列的选取,对于最优的增量序列,希尔排序的时间复杂度可以达到O(n1.3)左右,比普通的插入排序O(n2)要好很多。但是,由于希尔排序的增量序列选择是一个尚未解决的数学问题,所以希尔排序的实际性能可能会因增量序列的不同而有所差异。在实际应用中,常用的增量序列有Hibbard增量序列、Sedgewick增量序列等。
希尔排序是插入排序的一种优化,它通过将比较的全部元素分为几个区域来提升插入排序的性能,使得算法在数据量大的时候也能有较好的表现。
比如如下图所示,有无无序列:
84、83、88、87、61、50、70、60、80、99
第一遍,先取间隔为(Δ=5Δ=5),即依次对以下5组数据进行排序:
84、83、88、87、61、50、70、60、80、99
84、83、88、87、61、50、70、60、80、99
84、83、88、87、61、50、70、60、80、99
84、83、88、87、61、50、70、60、80、99
84、83、88、87、61、50、70、60、80、99
注意,当对84和50进行排序时,其他的元素就像不存在一样。因此,经过上述间隔为5的一遍排序后,数据如下:
50、83、88、87、61、84、70、60、80、99
50、70、88、87、61、84、83、60、80、99
50、70、60、87、61、84、83、88、80、99
50、70、60、80、61、84、83、88、87、99
50、70、60、80、61、84、83、88、87、99
最终的结果(50、70、60、80、61、84、83、88、87、99)是经过这一遍间隔Δ=5Δ=5的情况下达成的,接下去缩小间隔重复如上过程。例如让间距Δ=3Δ=3:
50、70、60、80、61、84、83、88、87、99
50、70、60、80、61、84、83、88、87、99
50、70、60、80、61、84、83、88、87、99
50、70、60、80、61、84、83、88、87、99
将上述粗体的每一组数据进行排序,得到:
50、70、60、80、61、84、83、88、87、99
50、61、60、80、70、84、83、88、87、99
50、61、60、80、70、84、83、88、87、99
50、61、60、80、70、84、83、88、87、99
最终的结果(50、61、60、80、70、84、83、88、87、99)更加接近完全有序的序列。接下去继续不断减小间隔,最终令Δ=1Δ=1,确保每一个元素都在恰当的位置。动图展示如下:
示例代码:
#include <stdio.h>
void shellSort(int arr[], int n)
{
int gap, i, j, temp;
// 初始增量设为数组长度的一半,之后每次减半
for (gap = n / 2; gap > 0; gap /= 2)
{
// 对每个子序列进行插入排序
for (i = gap; i < n; i++)
{
temp = arr[i];
// 从当前元素开始,比较前一个间隔为gap的元素
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap)
{
// 如果前一个元素比当前元素大,则交换它们
arr[j] = arr[j - gap];
}
// 找到合适的位置,插入当前元素
### 最后的话
最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!
### 资料预览
给大家整理的视频资料:
![](https://img-blog.csdnimg.cn/img_convert/f26c3672f21a8819db7034b44b2c58fe.png)
给大家整理的电子书资料:
![](https://img-blog.csdnimg.cn/img_convert/19b475a97eec20e295dc831042ddf677.png)
**如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!**
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以点击这里获取!](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
// 找到合适的位置,插入当前元素
### 最后的话
最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!
### 资料预览
给大家整理的视频资料:
[外链图片转存中...(img-WGW89LFO-1715290686260)]
给大家整理的电子书资料:
[外链图片转存中...(img-Gie8B39L-1715290686260)]
**如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!**
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以点击这里获取!](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**