内部排序-直接插入排序·冒泡排序·快速排序对比
写在前面:参考《数据结构(C语言版)》严蔚敏 吴伟民 编著 清华大学出版社 2008年10月第27次印刷
📋 算法概述
直接插入排序(Straight Insertion Sort)是一种最简单的排序方法,它的基本操作是将一个记录插入到已排号序的有序表中,从而得到一个新的、记录数增1的有序表。
冒泡排序(参考书里面叫作起泡排序)(Bubble Sort)。重复地遍历待排序序列,依次比较相邻的两个元素,如果它们的顺序错误就把它们交换过来。每次遍历都会将当前未排序部分的最大(或最小)元素“浮”到顶端,如同水中的气泡一样。
快速排序(Quick Sort)是对起泡排序的一种改进。采用分治法。首先选择一个基准元素,通过一趟排序将待排序列分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据要小。然后再按此方法对这两部分数据分别进行快速排序,整个过程递归进行。
🎯 算法特点
| 特性 | 直接插入排序 | 冒泡排序 | 快速排序 |
|---|---|---|---|
| 核心思想 | 插入 | 交换 | 分治 |
| 时间复杂度(平均) | O(n²) | O(n²) | O(n log n) |
| 时间复杂度(最好) | O(n) (序列已基本有序) | O(n) (序列已有序) | O(n log n) |
| 时间复杂度(最坏) | O(n²) (序列逆序) | O(n²) (序列逆序) | O(n²) (基准选择不当,如序列已有序) |
| 空间复杂度 | O(1) | O(1) | O(log n) ~ O(n) (递归栈的深度) |
| 稳定性 | 稳定 | 稳定 | 不稳定 |
| 排序方式 | In-Place (内部排序) | In-Place (内部排序) | In-Place (内部排序) |
| 优点 | 1. 算法简单。 2. 对基本有序的序列效率极高。 3. 原地排序,空间效率高。 |
1. 算法极其简单,易于理解。 2. 原地排序,空间效率高。 |
1. 平均性能极快,是基于比较的内部排序中最好的方法之一。 2. 原地排序,缓存友好。 |
| 缺点 | 平均和最坏情况时间复杂度高,不适合大规模乱序数据。 | 效率非常低下,移动次数多,实际应用中几乎不被使用。 | 1. 最坏情况性能较差。 2. 递归实现需要额外的栈空间。 3. 不稳定。 |
| 适用场景 | 1. 数据量较小。 2. 序列基本有序。 3. 作为高级排序算法的子过程(如快速排序优化后的小数组用插入排序)。 |
主要用于教学,帮助理解排序概念。实际应用中几乎不会被使用。 | 1. 适用于大规模、乱序的数据集,是应用最广泛的通用排序算法。 2. 对时间复杂度要求高的场景。 |
🔍 算法原理
待补充
💻 C# 实现代码
🚀示例
using System.Diagnostics;
namespace CsharpOne
{
internal class Test0912
{
private delegate void DoSort(List<int> list);
public void JustDoIt()
{
try
{
List<int> list = new List<int>() { 49, 38, 99, 13, 49, 57 };
//list = new List<int>() { 11, 22, 33, 44, 55, 66 };
//list = new List<int>();
for (int i = 0; i < 10000; i++)
{
list.Add(new Random().Next(1, 10000));
//list.Add(i + 100);
//list.Add(100000 - i);
}
Console.WriteLine(string.Join(",", list));
List<int> list1 = [.. list];
List<int> list2 = [.. list];
List<int> list3 = [.. list];
DoSort doSort1 = DoStraightInsertionSort;
DoSort doSort2 = DoBubbleSort;
DoSort doSort3 = DoQuickSort;
DoStopwatch(doSort1, list1);
DoStopwatch(doSort2, list2);
DoStopwatch(doSort3, list3);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}
}
private void DoStopwatch(DoSort doSort, List<int> list)
{
Stopwatch sw = Stopwatch.StartNew();
sw.Start();
var memory1 = GC.GetTotalMemory(true);
doSort(list);// 通过委托调用排序方法
Console.WriteLine($"{doSort.GetType().Name}****{doSort.Method.Name}************************");
//Console.WriteLine(string.Join(",", list));
var memory2 = GC.GetTotalMemory(true);
Console.WriteLine($"memory1:{memory1}, memory2:{memory2}, diff:{memory2 - memory1} bytes");
sw.Stop();
Console.WriteLine($"time:{sw.ElapsedMilliseconds} ms");
}
private void DoStraightInsertionSort(List<int> list)
{
StraightInsertionSort(list);
}
private void DoBubbleSort(List<int> list)
{
BubbleSort(list);
}
private void DoQuickSort(List<int> list)
{
QuickSort(list, 0, list.Count - 1);// list.Sort();
}
/// <summary>
/// 对 list 直接插入排序 从小到大
/// </summary>
/// <param name="list">数据列表</param>
private void StraightInsertionSort(List<int> list)
{
if (list == null || list.Count == 0) return;
/**初始化:49, 38, 99, 13, 49, 57
* 第一趟: 38, 49, 99, 13, 49, 57
* 第二趟: 38, 49, 99, 13, 49, 57
* 第三趟: 38, 49, 空, 99, 49, 57
* 第三趟: 38, 空, 49, 99, 49, 57
* 第三趟: 空, 38, 49, 99, 49, 57
* 第三趟: 13, 38, 49, 99, 49, 57
* 第四趟: 13, 38, 49, 49, 99, 57
* 第五趟: 13, 38, 49, 49, 57, 99
*/
for (int i = 1; i < list.Count; i++)
{
int current = list[i];
int j = i - 1;
// 移动元素而不是交换
while (j >= 0 && list[j] > current)
{
list[j + 1] = list[j];
j--;
}
list[j + 1] = current;
}
}
/// <summary>
/// 对 list 冒泡排序 从小到大
/// </summary>
/// <param name="list">数据列表</param>
private void BubbleSort(List<int> list)
{
if (list == null || list.Count == 0) return;
/**初始化:49, 38, 99, 13, 49, 57
* 第一趟: 38, 49, 99, 13, 49, 57
* 第一趟: 38, 49, 99, 13, 49, 57
* 第一趟: 38, 49, 13, 99, 49, 57
* 第一趟: 38, 49, 13, 49, 99, 57
* 第一趟: 38, 49, 13, 49, 57, 99
*
* 第二趟: 38, 49, 13, 49, 57, 99
* 第二趟: 38, 13, 49, 49, 57, 99
* 第二趟: 38, 13, 49, 49, 57, 99
* 第二趟: 38, 13, 49, 49, 57, 99
*
* 第三趟: 13, 38, 49, 49, 57, 99
* 第三趟: 13, 38, 49, 49, 57, 99
* 第三趟: 13, 38, 49, 49, 57, 99
*
* 第四趟: 13, 38, 49, 49, 57, 99
* 第四趟: 13, 38, 49, 49, 57, 99
*
* 第五趟: 13, 38, 49, 49, 57, 99
*/
int flagData = 0;
bool isSort = true;
for (int i = 0; i < list.Count - 1; i++)
{
isSort = true;
for (int j = 0; j < list.Count - i - 1; j++)
{
if (list[j] > list[j + 1])
{
isSort = false;
flagData = list[j];
list[j] = list[j + 1];
list[j + 1] = flagData;
}
}
if (isSort)// 若存在一次遍历发现已排序好,则无需继续遍历
return;
}
}
/// <summary>
/// 对 list 进行快速排序 从小到大
/// </summary>
/// <param name="list">数据列表</param>
/// <param name="left">左侧对比下标</param>
/// <param name="right">右侧对比下标</param>
private void QuickSort(List<int> list, int left, int right)
{
if (list == null || list.Count == 0 || left < 0 || right >= list.Count) return;
/**初始化:49, 38, 99, 13, 49, 57
* 第一趟: P P位置的在下标为0,指向49
* 第一趟: 49, 38, 99, 13, 49, 57
* L R 首先移动R①
* 第一趟: 49, 38, 99, 13, 49, 57
* L R R位置的57不比49小,R左移
* 第一趟: 49, 38, 99, 13, 49, 57
* L R R位置的49不比49小,R左移
* 第一趟: 13, 38, 99, 空, 49, 57
* L R R位置的13比49小,相当于将13和49调换位置
* 第一趟: 13, 38, 99, 空, 49, 57
* L R 然后移动L②
* 第一趟: 13, 38, 99, 空, 49, 57
* L R L位置的38不比49大,L右移
* 第一趟: 13, 38, 空, 99, 49, 57
* L R L位置的99比49大,相当于将99和49调换位置
* 第一趟: 13, 38, 49, 99, 49, 57
* LR 再次移动R,发现R和L重合(若R和L没有重合就重复步骤①②,直到R和L重合),停止
* 第一趟: P(49)左边都是小于49,P(49)右边都是大于或者等于49。返回Pivot = 2
*
* 递归调用P左边
* 第二趟: P P位置的在下标为0,指向13
* 第二趟: 13, 38
* L R 首先移动R
* 第二趟: 13, 38
* LR R位置的38不比13小,R左移
* 第二趟: 发现R和L重合,停止
* 第二趟: P(13)左边都是小于13,P(13)右边都是大于或者等于13。返回Pivot = 0
*
* 递归调用P右边
* 第三趟: P P位置的在下标为0,指向99
* 第三趟: 99, 49, 57
* L R 首先移动R
* 第三趟: 57, 49, 空
* L R R位置的57比99小,相当于将57和99调换位置
* 第三趟: 57, 49, 空
* L R 然后移动L
* 第三趟: 57, 49, 99
* LR 发现R和L重合,停止。返回Pivot = 5
*
* 第四趟: P P位置的在下标为0,指向57
* 第四趟: 57, 49
* 第四趟: L R 首先移动R
* 第四趟: 49, 空 R位置的49比57小,相当于将49和57调换位置
* 第四趟: L R 然后移动L
* 第四趟: 49, 57 R位置的49比57小,相当于将49和57调换位置
* 第四趟: LR 发现R和L重合,停止。返回Pivot = 4
*
*
* 最终修改集合为: 13, 38, 49, 49, 57, 99
*/
if (left < right)
{
int pivot = GetPivot(list, left, right);// 获取枢轴
QuickSort(list, left, pivot - 1);// 对枢轴左侧进行快速排序
QuickSort(list, pivot + 1, right);// 对枢轴右侧进行快速排序
}
}
/// <summary>
/// 获取枢轴(列表下标)
/// </summary>
/// <param name="list">数据列表</param>
/// <param name="left">左侧对比下标</param>
/// <param name="right">右侧对比下标</param>
/// <returns></returns>
private int GetPivot(List<int> list, int left, int right)
{
int pivot = list[left];
while (left < right)
{
while (left < right && list[right] >= pivot)
{
right--;
}
list[left] = list[right];
while (left < right && list[left] < pivot)
{
left++;
}
list[right] = list[left];
}
list[left] = pivot;
return left; // 返回枢轴位置,列表下标
}
}
}
代码移位了,截图辅助理解



耗时对比
10000数据,多次调用排序并计时。耗时单位为ms。生成的都是从小到大的顺序。
| 排序 | 随机数 | 随机数 | 随机数 | 顺序数 | 顺序数 | 顺序数 | 逆序数 | 逆序数 | 逆序数 |
|---|---|---|---|---|---|---|---|---|---|
| 直接插入排序 | 489 | 309 | 423 | 16 | 9 | 15 | 779 | 637 | 706 |
| 冒泡排序 | 1549 | 1436 | 1587 | 5 | 2 | 4 | 1714 | 1799 | 1843 |
| 快速排序 | 8 | 6 | 25 | 369 | 343 | 316 | 328 | 331 | 399 |
📝 实践
扩展思考题:
- 如何修改算法实现降序排序?
- 使用不同举例感受排序过程、稳定性(11,22,33,44,55或者55,44,33,22,11或者11,33,22,33,22等)?
- 在实际项目中,什么情况下会选择使用何种排序?
浙公网安备 33010602011771号