文章目录
- 一、冒泡排序(Bubble Sort)
- 1.基本思想
- 2.动图演示
- 3.算法描述
- 4.代码实现
- 二、快速排序(Quick Sort)☆
- 1.基本思想
- 2.动图演示
- 3.算法描述
- 4.代码实现
- 三、选择排序(Selection Sort)
- 1.基本思想
- 2.动图演示
- 3.算法描述
- 4.代码实现
- 四、堆排序(Heap Sort)
- 1.基本思想
- 2.动图演示
- 3.算法描述
- 4.代码实现
- 五、插入排序(Insertion Sort)
- 1.基本思想
- 2.动图演示
- 3.算法描述
- 4.代码实现
- 六、希尔排序(Shell Sort)
- 1.基本思想
- 2.动图演示
- 3.算法描述
- 4.代码实现
- 七、归并排序(Merge Sort)
- 1.基本思想
- 2.动图演示
- 3.算法描述
- 4.代码实现
- 八、基数排序(Radix Sort)
- 1.基本思想
- 2.动图演示
- 3.算法描述
- 4.代码实现
- 九、计数排序(Counting sort)
- 1.基本思想
- 2.动图演示
- 3.算法描述
- 4.代码实现
- 十、桶排序(Bucket sorting)
- 1.基本思想
- 2.动图演示
- 3.算法描述
- 4.代码实现
- 十一、外部排序小专题
- 1.外部排序原理
- 2.外部排序开销及应用场景
- 2.多路归并(提高外排效率)
- 3.多路平衡归并与多路归并区别
- 4.败者树(优化关键字对比次数)
- 5.置换-选择排序(优化初始构造归并段的数量)
- 6.最佳归并树(本质:多元哈弗曼树)
- 十二、根据算法的特性划分
- 1.根据算法是否稳定划分
- 2.根据内/外部排序划分
- 3.初始序列对算法有影响
- 4.是否是比较型
- 总结
- 附录:排序算法学习工具
- 1.usf算法动画演示
- 2.ChatGTP
- 3.Claude
一、冒泡排序(Bubble Sort)
1.基本思想
两个数比较大小,较大的数下沉,较小的数冒起来。
2.动图演示
3.算法描述
下面是冒泡排序的一般步骤:
- 从序列的第一个元素开始,比较相邻的两个元素。
- 如果顺序不对(例如,当前元素大于下一个元素),则交换它们。
- 继续遍历序列,重复上述比较和交换过程,直到整个序列有序。
- 重复以上步骤,每次遍历都将序列中的最大元素移动到末尾。
4.代码实现
void BubbleSort(int *p){
for(int i=0;i<maxSize-1;i++){
for (int j=0;j<maxSize-i-1;j++){
if(p[j]>p[j+1]){
int temp=p[j];
p[j]=p[j+1];
p[j+1]=temp;
}
}
}
}
二、快速排序(Quick Sort)☆
1.基本思想
基本思想:(分治)
- 先从数列中取出一个数作为key值;
- 将比这个数小的数全部放在它的左边,大于或等于它的数全部放在它的右边;
- 对左右两个小数列重复第二步,直至各区间只有1个数。
2.动图演示
3.算法描述
以下是快速排序的一般步骤:
- 选择基准元素: 从数组中选择一个基准元素,通常是数组中的第一个元素。
- 分区(Partition): 将数组中的元素按照基准元素的大小分为两部分。一个部分是所有小于基准元素的元素,另一个部分是所有大于基准元素的元素。基准元素此时在其最终排序的位置。
- 递归排序: 递归地对基准元素两侧的子序列进行排序。
- 合并: 不需要合并步骤,因为在分区步骤中已经确定了基准元素的最终位置。
快排思想广泛适用于408专业课,只要是涉及线性表,80%以上的问题可用快排快速结束战斗。
4.代码实现
int initFastSort(int *p,int l,int r){
int temp=p[l];
while(l<r){
while (l<r&&p[r]>=temp)//先缩小右域便于操作
--r;
p[l]=p[r];
while (l<r&&p[l]<=temp)
++l;
p[r]=p[l];
}
p[l]=temp;
return l;
}
void FastSort(int *p,int l,int r){
if(l<r){
int mid=initFastSort(p,l,r);
FastSort(p,l,mid-1);
FastSort(p,mid+1,r);
}
}
void StartFastSort(int *p){
FastSort(p,0,maxSize-1);
}
三、选择排序(Selection Sort)
1.基本思想
在长度为N的无序数组中,第一次遍历n-1个数,找到最小的数值与第一个元素交换;
第二次遍历n-2个数,找到最小的数值与第二个元素交换;
。。。
第n-1次遍历,找到最小的数值与第n-1个元素交换,排序完成。
2.动图演示
3.算法描述
以下是选择排序的一般步骤:
- 初始状态: 将整个数组分为已排序部分和未排序部分。初始时,已排序部分为空,未排序部分包含所有元素。
- 找到最小元素: 从未排序部分选择最小的元素。
- 交换位置: 将选中的最小元素与未排序部分的第一个元素交换位置。
- 更新已排序部分和未排序部分: 将已排序部分的末尾扩展一个元素,将未排序部分的第一个元素剔除,使得已排序部分增加一个元素,未排序部分减少一个元素。
- 重复步骤2-4: 重复执行以上步骤,直到整个数组有序。
4.代码实现
void SelectSort(int *p){
for(int i=0;i<maxSize-1;i++){
for(int j=i+1;j<maxSize;j++){
if(p[i]>p[j]){
int temp=p[i];
p[i]=p[j];
p[j]=temp;
}
}
}
}
四、堆排序(Heap Sort)
1.基本思想
适用于数组存储,使用链式存储会降低效率,对每一个节点而言,保证其左右子树均比其大或者小,排序过程中时刻保持堆的特性。排序的过程是每次拿走堆顶元素放入有序序列,然后把最后一行的最后一个元素堆到堆顶,此时堆的特性遭到破坏,下沉堆顶元素,直至恢复堆的特性,依次重复,直到堆内没有元素。
堆数据结构遇到新插入的元素放在整棵树最后一层的最后一个节点,然后依次往上浮(不用考虑从上面换下来的元素,因为原来堆结构稳定,换下来的元素依旧比下面的元素大)
2.动图演示
3.算法描述
- 构建堆(Heapify): 将待排序的序列视为一个完全二叉树,并将其转换为一个堆。对于最大堆,可以通过从最后一个非叶子节点开始向前遍历,对每个节点进行堆调整操作,保证每个节点都满足堆的性质。
- 堆排序: 通过反复从堆中取出最大(或最小)元素,并将其放到数组的末尾。每次取出元素后,需要对剩余元素进行堆调整,以保持堆的性质。
4.代码实现
void DeliverBigHeap(int *p,int k,int maxIndex){
int sonIndex=k*2+1;
while (sonIndex<=maxIndex){
if(sonIndex+1<=maxIndex&&p[sonIndex]<p[sonIndex+1]){
sonIndex++;
}
if(p[k]<p[sonIndex]){
int temp=p[k];
p[k]=p[sonIndex];
p[sonIndex]=temp;
// 交换之后继续探测上一个根节点
k=sonIndex;
sonIndex=k*2+1;
}else{
break;
}
}
}
void HeapSort(int *p,int len){
int temp;
for(int i=(len-1)/2;i>=0;i--){
DeliverBigHeap(p,i,len-1);
}
temp=p[len-1];
p[len-1]=p[0];
p[0]=temp;
for(int i=len-2;i>0;i--){
DeliverBigHeap(p,0,i);
temp=p[i];
p[i]=p[0];
p[0]=temp;
}
}
五、插入排序(Insertion Sort)
1.基本思想
在要排序的一组数中,假定前n-1个数已经排好序,现在将第n个数插到前面的有序数列中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。
2.动图演示
3.算法描述
- 初始状态: 将第一个元素视为已排序序列,其余元素为未排序序列。
- 遍历未排序序列: 从第二个元素开始,逐个将未排序序列的元素插入到已排序序列的适当位置,以保持已排序序列的有序性。
- 插入操作: 对于每个要插入的元素,与已排序序列的元素比较,找到插入位置,并将元素插入到该位置。插入过程中,已排序序列的元素可能需要后移,为新元素腾出空间。
- 重复步骤2-3: 重复进行插入操作,直到未排序序列为空。
4.代码实现
void InsertSort(int *p){
for(int i=1;i<maxSize;i++){
int temp=p[i];
int j=i-1;
while (j>=0&&p[j]>temp){
p[j+1]=p[j];
j--;
}
p[j+1]=temp;
}
}
六、希尔排序(Shell Sort)
1.基本思想
在要排序的一组数中,根据某一增量分为若干子序列,并对子序列分别进行插入排序。
然后逐渐将增量减小,并重复上述过程。直至增量为1,此时数据序列基本有序,最后进行插入排序。增量大小会影响到排序的效率。
2.动图演示
本图引用自博客园。
3.算法描述
- 选择间隔序列: 选择一个递减的间隔序列,通常以 n/2、n/4、n/8… 等为步长,直到步长为 1。
- 对每个间隔进行插入排序: 对数组中每个间隔所形成的子序列进行插入排序。
- 缩小间隔: 不断缩小间隔,重复步骤2,直到最终间隔为 1。
4.代码实现
function shellSort(arr):
n = length of arr
// 选择初始间隔
gap = n / 2
// 缩小间隔直至为 1
while gap > 0:
// 对每个间隔进行插入排序
for i from gap to n - 1:
temp = arr[i]
j = i
// 对当前间隔的子序列进行插入排序
while j >= gap and arr[j - gap] > temp:
arr[j] = arr[j - gap]
j = j - gap
arr[j] = temp
// 缩小间隔
gap = gap / 2
// 示例
arr = [12, 34, 54, 2, 3]
shellSort(arr)
七、归并排序(Merge Sort)
1.基本思想
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。首先考虑下如何将2个有序数列合并。这个非常简单,只要从比较2个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。
2.动图演示
3.算法描述
- 划分: 将待排序的序列划分为两个子序列,直到每个子序列的长度为 1。
- 递归排序: 递归地对每个子序列进行排序。
- 合并: 将排好序的子序列合并成一个整体有序的序列。
4.代码实现
void Merger(int *p,int l,int midde,int r){
static int B[maxSize];
int i=0,j=0,z=0;
//最后将pB中的元素放进p;
for(i=l;i<=r;i++){
B[i]=p[i];
}
for(i=l,j=midde+1,z=l;i<=midde&&j<=r;z++){
if(B[i]<B[j]){
p[z]=B[i];
i++;
}else{
p[z]=B[j];
j++;
}
}
while(j<=r){
p[z++]=B[j++];
}
while(i<=midde){
p[z++]=B[i++];
}
}
void MergerSort(int *p,int l,int r){
if(l<r){
// 这个midde始终指向中间位置
int midde=(l+r)/2;
MergerSort(p,l,midde);
MergerSort(p,midde+1,r);
//合并序列
Merger(p,l,midde,r);
}
}
八、基数排序(Radix Sort)
1.基本思想
基数排序是按照低位先排序,然后收集;再按照次低位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
2.动图演示
3.算法描述
- 初始状态: 将所有待排序的整数统一为同样的位数,位数较短的整数在高位补零。
- 按最低位排序: 从最低位开始,按照该位的值将所有元素分配到相应的桶中。
- 合并桶: 将所有桶中的元素按照顺序合并成一个序列。
- 按次低位排序: 对上一步合并后的序列,按照次低位的值将所有元素分配到相应的桶中。
- 重复步骤3-4: 重复进行桶合并和按位排序,直到所有位数都考虑过。
4.代码实现
function radixSort(arr):
// 获取数组中最大元素的位数
maxNum = findMax(arr)
digit = 1
// 对每个数字位进行排序
while maxNum / digit > 0:
countingSort(arr, digit)
digit = digit * 10
function countingSort(arr, digit):
n = length of arr
output = [0] * n // 用于存放排序后的结果
count = [0] * 10 // 由于每个数字位的值范围是 0-9,创建一个计数数组
// 统计每个数字位上的元素个数
for i from 0 to n - 1:
index = (arr[i] / digit) % 10
count[index] = count[index] + 1
// 将计数数组转换为位置数组
for i from 1 to 9:
count[i] = count[i] + count[i - 1]
// 根据位置数组将元素放入输出数组中
i = n - 1
while i >= 0:
index = (arr[i] / digit) % 10
output[count[index] - 1] = arr[i]
count[index] = count[index] - 1
i = i - 1
// 将排序后的结果复制回原数组
for i from 0 to n - 1:
arr[i] = output[i]
function findMax(arr):
// 找到数组中的最大值
maxNum = arr[0]
for num in arr:
if num > maxNum:
maxNum = num
return maxNum
// 示例
arr = [170, 45, 75, 90, 802, 24, 2, 66]
radixSort(arr)
九、计数排序(Counting sort)
1.基本思想
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
2.动图演示
3.算法描述
- 统计频次: 统计每个整数出现的次数,生成一个计数数组。
- 计算累加频次: 对计数数组进行累加,得到每个整数在排序后的序列中的位置。
- 生成排序序列: 根据累加频次信息,将原始数组中的元素放置到排序后的位置。
4.代码实现
function countingSort(arr):
// 找到数组中的最大值,确定计数数组的大小
maxNum = findMax(arr)
// 初始化计数数组和输出数组
count = [0] * (maxNum + 1)
output = [0] * length of arr
// 统计每个整数的频次
for num in arr:
count[num] = count[num] + 1
// 计算累加频次
for i from 1 to maxNum:
count[i] = count[i] + count[i - 1]
// 生成排序序列
for i from length of arr - 1 to 0:
num = arr[i]
output[count[num] - 1] = num
count[num] = count[num] - 1
// 将排序后的结果复制回原数组
for i from 0 to length of arr - 1:
arr[i] = output[i]
function findMax(arr):
// 找到数组中的最大值
maxNum = arr[0]
for num in arr:
if num > maxNum:
maxNum = num
return maxNum
// 示例
arr = [4, 2, 7, 1, 9, 3, 5]
countingSort(arr)
十、桶排序(Bucket sorting)
1.基本思想
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。
桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每
个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排
最终得出有序序列:3 9 21 25 29 37 43 49
2.动图演示
网上找了半天也没有找到相关的动图,使用usf大学的工具吧:桶排序-点击传送
还有其他算法动画的使用方法,我会放在文末。
3.算法描述
- 确定桶的数量: 根据待排序数据的范围和分布情况,确定需要的桶的数量。
- 将元素分配到桶中: 遍历待排序数组,根据元素的值将其分配到相应的桶中。
- 对每个桶进行排序: 对每个非空的桶中的元素使用其他排序算法(通常是插入排序或归并排序)进行排序。
- 合并桶: 将各个桶中的元素按顺序合并成一个有序序列。
4.代码实现
function bucketSort(arr):
n = length of arr
// 确定桶的数量
numBuckets = determineNumberOfBuckets(arr)
// 初始化桶
buckets = [[] for i from 0 to numBuckets - 1]
// 将元素分配到桶中
for i from 0 to n - 1:
bucketIndex = mapToBucket(arr[i], numBuckets)
buckets[bucketIndex].append(arr[i])
// 对每个桶进行排序
for i from 0 to numBuckets - 1:
sortBucket(buckets[i])
// 合并桶
sortedArray = mergeBuckets(buckets)
// 将排序后的结果复制回原数组
for i from 0 to n - 1:
arr[i] = sortedArray[i]
function determineNumberOfBuckets(arr):
// 根据待排序数据的范围和分布情况,确定桶的数量
// 这里可以根据实际情况选择不同的策略
return someFunctionOf(arr)
function mapToBucket(value, numBuckets):
// 根据元素的值将其映射到相应的桶中
// 这里可以选择不同的映射策略
return someFunctionOf(value, numBuckets)
function sortBucket(bucket):
// 对每个非空的桶中的元素进行排序
// 这里可以选择插入排序、归并排序等排序算法
someSortingAlgorithm(bucket)
function mergeBuckets(buckets):
// 合并各个桶的结果
// 这里可以选择简单的合并方法,如连接数组
mergedArray = []
for bucket in buckets:
mergedArray.extend(bucket)
return mergedArray
// 示例
arr = [0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68]
bucketSort(arr)
十一、外部排序小专题
1.外部排序原理
归并排序,总体来说分为两步:
- 初始化归并段(归并段数量影响着归并树的层数)
- 各路进行归并(归并的路数影响单次归并比较次数)
开始归并
第一趟归并完成!
依次类推。
2.外部排序开销及应用场景
外部排序应用于数据量特别大时。一般情况下,需要注意的是内存(主存)与外存(辅存)交换数据时间远大于内部排序时间。
2.多路归并(提高外排效率)
- 优化方法一:增加归并路数
- 代价一:增加相应的输入缓冲区数量
- 代价二:每次k个归并段选出一个元素要进行(k-1)次对比,k为归并路数
- 优化方法二:减少初始归并段数量
3.多路平衡归并与多路归并区别
4.败者树(优化关键字对比次数)
对比次数快速下降!变成树高
5.置换-选择排序(优化初始构造归并段的数量)
选择-置换排序构造归并段
开辟新的归并段。
6.最佳归并树(本质:多元哈弗曼树)
1.性质和构造完全相同于哈弗曼树
2.与哈弗曼树的区别:k叉树,其中k > 2时:需要判断是否能满足构造完全k叉树,若不满足,则需要添加长度为0的“虚段”
- ①若(初始归并段数量 - 1) % (k - 1) = 0,则能构成完全k叉树
- ②若(初始归并段数量 - 1) % (k - 1)= u ≠ 0,则说明需要添加(k - 1)- u 个虚段才能构成完全二叉树
十二、根据算法的特性划分
1.根据算法是否稳定划分
稳定性:相同的元素经过排序后,位置没有改变则称其稳定,否则称不稳定
2.根据内/外部排序划分
内/外部排序:如果数据量过大,没有办法全部加载进内存就选用外部排序。
3.初始序列对算法有影响
看总结中的表格,这个特性是常考察的一个点。
4.是否是比较型
总结
排序算法 | 平均时/空复杂度; 最优时间复杂度;最差时间复杂度 | 初始序列对算法的影响 | 排序思路特点 | 备注 |
---|---|---|---|---|
冒泡 | O(n2)/O(1);O(n);O(n2) | 初始逆序最慢,初始正序最快(只需进行比较) | 两两比较,大的后移,一次能定一个元素的最终位置(与选择排序不同的是选出的元素一直在动) | 喜欢结合算法特性对比考察-选择 |
快速 | O(nlogn)/O(logn);O(nlogn) ;O(n2) | 初始有序递归深度为n,越乱越有性价比 | 左小右大,一次确定一个元素最终位置 | 考频最高,一般考察算法思想的应用 |
插入 | O(n2)/O(1);O(n);O(n2) | 初始成序最快 | 前部先成序,选择后面的数字插到的成序的数列里面 | 喜欢结合算法特性对比考察-选择 |
希尔 | O(n1.2-2)/O(1);O(n);O(n2) | 初始逆序最慢,初始正序最快(只需进行比较) | 以一定间隔划分序列,各个子序列内使用插入排序,逐步缩小划分间隔,最终序列有序 | 喜欢结合算法特性对比考察-选择 |
选择 | O(n2)/O(1);O(n2);O(n2) | 无关,一定会比较O(n2)次 | 每次选出一个元素,与冒泡排序不同的是选出的元素一直处在固定位置,一次定一个位置 | 喜欢结合算法特性对比考察-选择 |
堆 | O(nlogn)/O(1);O(nlogn);O(nlogn) | 无关 | 建堆,插入,调整堆结构(下沉) | 喜欢n个元素中选出top n大或小的元素 |
归并 | O(nlogn)/O(n);O(nlogn);O(nlogn) | 无关 | 将序列划分为多个子序列,然后按路数合并 | 喜欢结合算法特性对比考察-选择 |
基数 | O(d×(n+r))/O(n+r);O(d×(n+r));O(d×(n+r)) | 无关 | 先个位,再十位再百位…(先低优先级,再高优先级) | 喜欢结合算法特性对比考察-选择 |
计数 | O(n+k)/O(n+k);O(n+k);O(n+k) | 无关 | 不是基于比较的排序算法 n是关键字的个数,k是关键字中最大值减最小值的变化差 | 喜欢结合算法特性对比考察-选择 |
桶 | O(n)/O(n+k);O(n) ;O(nlogn) | 无关 | 线性排序方法,k为划分的桶的个数 | 喜欢结合算法特性对比考察-选择 |
这里为了简化表格,以逆序代替与预期序列相反的序列,以正序代替与预期相同的序列。
例如:要对随机状态下的序列1433223进行元素从小到大的排序(升序)
- 预期:1223334
- 正序:1223334
- 逆序:4333221
附录:排序算法学习工具
1.usf算法动画演示
王道咸鱼学长基础课中分享的一个工具,挺好用的,包括多种综合性的算法动画演示:传送门
2.ChatGTP
学习好帮手,不会的算法直接问他就可以,思想代码回答的一清二楚,不会用的小伙伴可以看看这个教程,偶然在网上发现的:点击传送,在实战教程那章节有介绍怎么用。
3.Claude
学习好帮手,功能与ChatGPT很类似,所以就不详细介绍了,使用方法可以看我往期文章:点击传送
正片到这也就结束了,后续有补充的话再更新吧。