0%

数据结构复习笔记8-排序

9.内部排序


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 待排序的存储结构

#define MAXSIZE 20 // 顺序表最大长度

typedef int KeyType; // 定义关键字类型为整数类型

typedef struct{
KeyType key; // 关键字项
InfoType otherinfo; // 其他数据项
}RedType; // 记录类型

typedef struct{
RedType r[MAXSIZE + 1]; // r[0] 闲置或做哨兵单元
int length; // 顺序表长度
}SqList; // 顺序表类型

插入排序

直接插入


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 算法 10.1
// 对顺序表 L 作直接插入

void InsertSort(SqList &L)
{
for(i = 2; i <= L,length; ++i)
{
if(LT(L.r[i].key, L.r[i-1].key))// “小于”,需将L.r[i]插入到有序表(若大于,则无需以下操作,注意这个判定条件)
{
L.r[0] = L.r[i]; // 复制为“哨兵”
L.r[i] = L.r[i-1];
for(j = i - 2; LT(L.r[0].key, L.r[j].key); --j)
L.r[j + 1] = L.r[j]; // 记录后移
L.r[j + 1] = L.r[0]; // 插入到正确的位置
}
}
}

折半插入


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 算法 10.2
// 对顺序表 L 作折半插入排序

void BInsertSort(SqList &L)
{
for(i = 2; i <= L.length; ++i)
{
L.r[0] = L.r[i]; // 哨兵
low = 1; high = i - 1;

while(low <= high) // 在r[low...high]中折半查找有序插入的位置
{
m = (low + high) / 2;
if(LT(L.r[0].key, L.r[m].key))
high = m - 1; // 插入点在低半区
else
low = m + 1; // 插入点在高半区
}

for(j = i - 1; j >= high + 1; --j)
L.r[j + 1] = L.r[j]; // 记录后移

L.r[high + 1] = L.r[0]; // 插入(注意插入位置为 high + 1)
}
}

  • 性能分析
    • 空间复杂度:\(O(1)\)
    • 时间复杂度:\(O(n)\)

希尔排序


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 算法 10.4
// 对顺序表 L 做一趟希尔插入排序。本算法和一趟直接插入排序相比,
// 做了如下修改
// 1. 前后记录位置的增量是 dk,而不是 1
// 2. r[0] 知识暂存单元,不是哨兵。当 j <= 0 时,插入位置已找到

void ShellInsert(SqList &L, int dk)
{
for(i = dk + 1; i <= L.length; ++i)
{
if(LT(L.r[i],key, L.r[i - dk],key)) // 需将 L.r[i] 插入到有序表
{
L.r[0] = L.r[i]; // 暂存在 L,r[0]

for(j = i - dk; j > 0 && LT(L.r[0].key, L.r[j].key); j -= dk)
L.r[j + dk] = L.r[j]; // 记录后移,查找插入位置

L.r[j+dk] = L.r[0]; // 插入
}
}
}
1
2
3
4
5
6
7
8
9
10
// 算法 10.5
// 按增量序列 dlta[0...t-1]对顺序表 L 做希尔排序

void ShellSort(SqList &L. int dlta[], int t)
{
for(k = 0; k < t; ++k)
{
ShellInsert(L, dlta[k]); // 一趟增量为 dlta[k]的插入排序
}
}

交换排序

冒泡排序


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 用冒泡排序法将序列 A 中的元素按从小到大顺序排列

void BubbleSort(ElemType A[], int n)
{
for(i = 0; i < n - 1; i++)
{
flag = false; // 表示本趟冒泡是否发生交换的标志

for(j = n - 1; j > i; j--) // 一趟排序的过程
{
if(A[j-1].key > A[j].key) // 若为逆序
{
swap(A[j-1], A[j]); // 交换
flag = true;
}

if(flag = false)
return ; //(注意!!!) 本次遍历后没有发生交换,说明表已经有序。直接返回
}
}
}

快速排序


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 算法 10.6(b)
// 交换顺序表 L 中子表 r[low..high]的记录,枢轴记录到位,
// 并返回其所在位置,此时在它之前(后)的记录均不大(小)于它

int Partition(SqList &L, int low, int high)
{
L.r[0] = L.r[low];
pivotkey = L.r[low].key;

while(low < high) // 从表的两端交替地向中间扫描
{
while(low < high && L.r[high].key >= pivotkey)
--high;

L.r[low] = L.r[high]; // 将比枢轴记录小的记录移到低端

while(low < high && L.r[low].key <= pivotkey)
++low;

L.r[high] = L.r[low]; // 将比枢轴记录大的记录移到高端
}

L.r[low] = L.r[0]; // 枢轴记录到位

return low; // 返回枢轴位置
}
1
2
3
4
5
6
7
8
9
void QSort(SqList &L, int low, int high)
{
if(low < high) // 长度大于 1
{
pivotloc = Partition(L, low, high); // 将 L.r[low..high] 一分为二
QSort(L, low, pivotloc - 1); // 对低子表递归排序,pivotloc 是枢轴位置
QSort(L, pivotloc + 1, high); // 对高子表递归排序
}
}

选择排序

简单选择


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 算法 10.9
// 对顺序表 L 作简单选择排序

void SelectSort(SqList &L)
{
for(i = 1; i < L.length; ++i) // 选择第 i 小的记录并交换到位
{
min = i;
for(j = i + 1; j < n; j++) // 在 L.r[i..L.length]中选择 key 最小的记录
{
if(L.r[j].key < L.r[min].key)
min = j;
}

if(min != i) // 交换
swap(L[i], L[min]);
}
}

堆排序


1
typedef SqList HeapType;    // 堆采用顺序表存储表示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 算法 10.10
// 已知H.r[s..m]中记录的关键字除H.r[s].key之外均满足堆的定义,
// 本函数调整 H.r[s]的关键字,使H.r[s..m]成为一个大顶堆
//(对其中记录的关键字而言)

void HeapAdjust(HeapType &H, int s, int m)
{
rc = H.r[s];

for(j = 2 * s; j <= m; j *= 2) // 沿 key 较大的孩子结点向下筛选
{
if(j < m && LT(H.r[j].key, H[j+1].key))
++j; // j 为 key 较大的记录的下标

if(! LT(rc.key, H.r[j].key)) // rc 应插入在位置 s 记录的下标
break;

H.r[s] = H.r[j];
s = j;
}

H.r[s] = rc; // 插入
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 算法 10.11
// 对顺序表 H 进行堆排序

void HeapSort(HeapType &H)
{
for(i = H.length / 2; i > 0; --i) // 把 H.r[1..H.length] 建成大顶堆
{
HeapAdjust(H, i, H.length);
}

for(i = H.length; i > 1; --i)
{
swap(H.r[1], H.r[i]); // 将堆顶记录和当前未经排序子序列 H.r[1..i] 中最后一个记录相互交换
}

HeapAdjust(H, 1, i - 1); // 将 H.r[1..i-1] 重新调整为大顶堆
}

归并排序


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 算法 10.12
// 将有序的 SR[i..m] 和 SR[m+1..n] 归并为有序的 TR[i..n]

void Merge(RcdType SR[], RcdType &TR[], int i, int m, int n)
{
for(j = m + 1, k = i; i <= m && j <= n; ++k)// 将 SR 中记录由小到大地并入 TR
{
// 注: LQ 表示 ≤
if(LQ(SR[i].key, SR[j].key))
TR[k] = SR[i++];
else
TR[k] = SR[j++];
}

if(i <= m) TR[k..n] = SR[i..m]; // 将剩余的 SR[i..m] 复制到 TR
if(j <= n) TR[k..n] = SR[j..n]; // 将剩余的 SR[j..n] 复制到 TR
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 算法 10.13
// 将 SR[s..t] 归并排序为 TR1[s..t]

void MSort(RcdType SR[], RcdType &TR1[], int s, int t)
{
if(s == t)
TR1[s] = SR[s];
else
{
m = (s + t) / 2; // 平分
MSort(SR, TR2, s, m); // 递归排序
MSort(SR, TR2, m + 1, t); // 递归排序
Merge(TR2, TR1, s, m, t); // 合并
}
}

基数排序

  • 主要思想:依据关键字的各个位的大小依次排序

内部排序算法的比较和应用

算法种类 最好时间复杂度 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性
直接插入 \(O(n)\)(待排序序列为正序 \(O(n^2)\) \(O(n^2)\)(待排序序列为逆序 \(O(1)\)
冒泡 \(O(n)\)(待排序序列为正序 \(O(n^2)\) \(O(n^2)\)(待排序序列为逆序 \(O(1)\)
简单选择 \(O(n^2)\) \(O(n^2)\) \(O(n^2)\) \(O(1)\)
希尔 \(O(1)\)
快排 \(O(nlog_2n)\)(每次的枢轴划分把表等分为长度相近的两个子表) \(O(nlog_2n)\) \(O(n^2)\)(待排序序列有序基本有序 \(O(log_2n)\)
堆排序 \(O(nlog_2n)\) \(O(nlog_2n)\) \(O(nlog_2n)\) \(O(1)\)
2 路归并 \(O(nlog_2n)\) \(O(nlog_2n)\) \(O(nlog_2n)\) \(O(n)\)
基数 \(O(d(n+r))\) \(O(d(n+r))\) \(O(d(n+r))\) \(O(r)\)

部分算法手算小结

插入排序

直接插入

  • 把这个过程想象成抓牌,对牌排序的过程
    • 抓牌
    • 比较:和当前最后一个牌比较
      • 若大于,则插在当前有序表最后(算法流程中,此步不做)
      • 若小于,则进入下一步
    • 查找插入位置,并插入:有序表中从后往前顺序查找新抓到的牌在有序表中合适的位置,在此过程中融入了移动元素。找到位置后,元素也移动完了,可以直接插入。

折半插入

  • 和直接插入不同,折半插入将比较查找移动元素分离。
  • 整个流程概括如下:
    • 在当前有序表中折半查找新元素待插入的的位置(请关注一下算法中的插入位置)。
    • 以此插入位置,确定需要移动元素的范围,将元素移动到位。
    • 插入新元素

希尔排序

  • 以间隔将待排序列分组,组内直接插入排序
  • 缩小间隔,重复上述操作,直至间隔为 1。

交换排序

冒泡排序

  • 每趟从后往前,若相邻元素为逆序,则交换。

快速排序

  • 划分
    • 对照算法,手算模拟
  • 排序
    • 递归的进行

选择排序

简单选择

  • 在未有序序列部分,选择最小【从小到大排列】(或最大【从大到小】),依次与当前未有序的头部元素交换,完成一趟简单选择排序

堆排序(★★★★★★★★)

  • 在理解堆排序之前,先来理解以下堆的几个基本操作
    • 堆的建立
    • 堆的插入
    • 堆的删除

堆的建立(利用堆的向下调整)

  • 从堆的最后一个结点(即完全二叉树的最后一个叶子结点)开始循环,依次进行如下操作:
    • 找到其父亲结点(设为 P
    • 比较 P 和其两个孩子结点的大小关系(下述流程和向下调整(父和子比较,父向下调整)类似)
      • 如果满足堆的定义,则进行下一轮迭代(在向下调整中,此步直接 break,即对结点 P 的调整结束)
      • 如果不满足
        • 小根堆情况:选择 key 值较小的孩子(设为 C ),与父亲P 交换
        • 大根堆情况:选择 key 值较大的孩子(设为 C ),与父亲P 交换
        • (!!!)注意事项:经过此次交换,可能引起交换后父亲结点所在的那个结点(也就是交换前的孩子结点,即为 C )所在子树不满足堆的定义。此时,要进一步检查 P 和其左右子树的关系,依据条件进行交换操作,直至检查到叶子结点。

堆的插入(利用堆的向上调整)

  • 将新结点插入到堆的末端,利用向上调整,使满足堆的定义。
  • 向上调整(子和父比较,子向上调整)流程简述
    • 从新插入的结点开始循环,依次比较其与其父结点的大小关系
      • 若不满足,则交换位置
      • 否则,结束循环,当前位置即为新插入结点的满足堆定义的位置。

堆的删除(利用堆的向下调整)

  • 交换堆顶元素(即是完全二叉树的根)和堆的最后一个元素(及完全二叉树的最后一个叶子结点)(或者是删除堆顶的元素,用堆的最后一个元素补位)
  • 对当前堆(除最后一个元素)进行向下调整(类似堆的建立,但是开始结点和调整范围不同,这里的向下调整的结点范围是从 1 到 n - 1,即是从头向后,进行调整),使满足堆的定义

利用堆的建立、删除理解堆排序

  • 初始建堆
  • 依次对堆顶元素进行删除操作(选择交换元素,而非删除元素)
  • 最终对堆的完全二叉树进行层序遍历,即可得到有序序列。

归并排序

  • 仅讨论二路归并
    • 首先,将待排序序列分为两两一组,然后使得组内有序;
    • 接着,将上述有序组,两两一组,有序合并;
    • 重复第二步,直至所有序列合并成一组

解题技巧及重要结论

  • 已知希尔排序某一趟排序后的序列,如何确定本次使用的增量?
    • 抓住组内有序(从小到大或者从大到小)这个关键点,从间隔为 1 开始依次验证,直至找到满足的最小间隔,即为本趟希尔排序使用的增量。
  • 快排的重要性质
    • i 趟排序完成时,会有 i 个以上的数出现在它最终将要出现的位置(这个位置就是排序最终结果的位置),即它左边的数都比它小,它右边的数都比它大。(可用于判定某一序列是否是某趟快排后的序列)
  • 在含有 n 个关键字的小根堆中,关键字最大的记录可能存储在什么位置?
    • 小根堆 + 关键字最大的条件意味着,该记录在堆的叶子节点
    • 而堆是一棵完全二叉树,其叶子结点位置范围为 \([\lfloor\frac{n}{2}\rfloor+1, n]\)
    • 在叶子结点位置范围中的点都可能
  • 向一个具有 n 个结点的堆中插入一个新元素的时间复杂度为 \(O(log_2n)\) ,删除一个元素的时间复杂度为 \(O(log_2n)\)
  • 构建 n 个记录的初始堆,时间复杂度为 \(O(n)\)对比上条);对 n 个记录进行堆排序,最坏情况下其时间复杂度为 \(O(nlog_2n)\)
  • 初始序列基本有序时,插入排序比较次数较少。

参考资料

  1. 数据结构(C语言版).严蔚敏等
  2. 2020年数据结构考研复习指导.王道论坛