数据结构与算法系列文章目录
【数据结构与算法】data structures & algorithms 第一章:复杂度分析
【数据结构与算法】data structures & algorithms 第二章:基本概念
【数据结构与算法】data structures & algorithms 第三章:线性数据结构
【数据结构与算法】data structures & algorithms 第四章:树的数据结构
【数据结构与算法】data structures & algorithms 第五章:图的数据结构
【数据结构与算法】data structures & algorithms 第六章:各类常见的排序算法
【数据结构与算法】data structures & algorithms 第七章:散列表算法的初步运用
【数据结构与算法】data structures & algorithms 第八章:红黑树的理解与使用
目录
一、简单排序
- 前提
void X_Sort (ElementType A[], int N)- 大多数情况下,讨论从小到大的整数排序;
- N是正整数,表示元素的个数;
- 只讨论基于比较的排序(>=<有定义);
- 只讨论内部排序;
- 稳定性:任意两个相等的数据,排序前后的相对位置不发生改变
- 没有任何一种排序是任何情况下都表示最好的;
1、冒泡排序
- 每次遍历比较,将最大的元素移动到数组后面;
void Bubble_Sort(ElementType A[], int N)
{
for (int P = N - 1; p >= 0; --P)
{
flag = 0;
for (int i = 0; i < P; ++i)
{
if (A[i] > A[i + 1])
{
Swap(A[i], A[i + 1]);
flag = 1;
}
}
if (flag == 0) break; //全程无交换就退出循环
}
}
- 最好情况:顺序
T
=
O
(
N
)
T\ =\ O(N)
T = O(N)
最坏情况:逆序 T = O ( N 2 ) T\ =\ O(N^2) T = O(N2)
2、插入排序
- 每次遍历地拿出一个元素,与其前面的元素比较大小,放入合适的位置
void Insertion_Sort(ElementType A[], int N)
{
for (int p = 1; p < N; ++p)
{
int tmp = A[p];
for (int i = p; i > 0 && A[i - 1] > tmp; --i)
A[i] = A[i - 1]; //移出空位;
A[i] = tmp; //将该元素放入空位;
}
}
- 最好情况:顺序
T
=
O
(
N
)
T\ =\ O(N)
T = O(N)
最坏情况:逆序 T = O ( N 2 ) T\ =\ O(N^2) T = O(N2)
3、时间复杂度下界
- 对于下标 i < j i\ <\ j i < j,如果 A [ i ] > A [ j ] A[i]\ >\ A[j] A[i] > A[j],则称 ( i , j ) (i,\ j) (i, j)是一对逆序对ub(inversion)
- 交换2个相邻元素正好消去1个逆序对
- 插入排序:
T
(
M
,
I
)
=
O
(
N
+
I
)
T(M,I)\ =\ O(N+I)
T(M,I) = O(N+I)
- 如果顺序基本有序,则插入排序简短且高效
- 定理:任意N个不同元素组成的序列平均具有 N ( N − 1 ) / 4 N(N\ -\ 1)/4 N(N − 1)/4个序列对
- 定理:任何仅以交换相邻两元素来排序的算法,其平均时间复杂度为 Ω ( N 2 ) \Omega(N^2) Ω(N2)
- 这意味着:要提高算法效率,我们必须
- 每次不止1个逆序对;
- 每次交换相隔较远的2个元素;
4、希尔排序
-
希尔排序,也称为递减增量排序算法,是插入排序的一种更高效的改进版本,但它是非稳定排序算法
- 思想:先将整个待排序的记录序列分割成为若干子序列,分别进行直接插入排序,待整个序列中的记录基本有序时,再对全体记录进行依次直接插入排序
-
定义增量序列 D M > D M − 1 > ⋯ > D 1 = 1 D_M\ >\ D_{M-1}\ >\cdots>D_1\ =\ 1 DM > DM−1 >⋯>D1 = 1;最后一次排序必须是间隔为1的排序;
-
对每个 D k D_k Dk进行” D k − D_k- Dk−间隔“排序 ( k = M , M − 1 , ⋯ , 1 ) (k=M,M-1,\cdots,1) (k=M,M−1,⋯,1);
-
注意:” D k D_k Dk-间隔“有序的序列,在执行” D k − 1 D_{k-1} Dk−1-间隔“排序后,仍然是” D k D_k Dk-间隔“有序的;
-
原始希尔排序用到的增量序列
D M = ⌊ N / 2 ⌋ , D k = ⌊ D k + 1 / 2 ⌋ D_M\ =\ \lfloor N/2 \rfloor,\ D_k\ =\ \lfloor D_{k+1}/2 \rfloor DM = ⌊N/2⌋, Dk = ⌊Dk+1/2⌋;
void shell_sort(elementType A[], int N)
{
//希尔增量排序
for (int D = N/2; D > 0; D /= 2)
{
//插入排序
for (int P = D; P < N; ++P)
{
int tmp = A[P];
for (int i = P; i >= D && A[i - D] > tmp; i -= D)
{
A[i] = A[i - D];
}
A[i] = tmp;
}
}
}
-
最坏情况: T = Θ ( N 2 ) T\ =\ \Theta (N^2) T = Θ(N2)
-
增量元素如果不互质,则小增量可能根本不起作用;
- 更多增量序列
- Hibbard增量序列
- D k = 2 k − 1 D_k\ =\ 2^k-1 Dk = 2k−1 ——相邻元素互质
- 最坏情况: T = Θ ( N 3 / 2 ) T\ =\ \Theta (N^{3/2}) T = Θ(N3/2)
- 猜想: T a v g = O ( N 5 / 4 ) T_{avg}\ =\ O(N^{5/4}) Tavg = O(N5/4)
- Sedgewick增量序列
-
{
1
,
5
,
19
,
41
,
109
,
⋯
}
\{ 1,5,19,41,109, \cdots \}
{1,5,19,41,109,⋯}
—— 9 × 4 i − 9 × 2 i + 1 9\times 4^i-9\times 2^i + 1 9×4i−9×2i+1或 4 i − 3 × 2 i + 1 4^i - 3\times 2^i + 1 4i−3×2i+1 - 猜想: T a v g = O ( N 7 / 6 ) T_{avg}\ =\ O(N^{7/6}) Tavg = O(N7/6), T w o r s t = O ( N 4 / 3 ) T_{worst}\ =\ O(N^{4/3}) Tworst = O(N4/3)
-
{
1
,
5
,
19
,
41
,
109
,
⋯
}
\{ 1,5,19,41,109, \cdots \}
{1,5,19,41,109,⋯}
- Hibbard增量序列
5、堆排序
-
利用堆这种数据结构,所设计的排序算法;有两种排序方法:
- 最小堆:每个结点的值都小于或等于其子结点的值,在算法中用于降序排列;
- 最大堆:每个结点的值都大于或等于其子结点的值,在算法中用于升序排列;
-
最小堆的方式
- 时间复杂度: T ( N ) = O ( N log N ) T(N)\ =\ O(N\log N) T(N) = O(NlogN);
- 注意的是,这里需要额外 O ( N ) O(N) O(N)空间,并且复制元素需要时间;
void Heap_Sort(elementType A[], int N)
{
buildMinHeap(A); //O(N)
for (int i = 0; i < N; ++i)
{
tmpA[i] = deleteMin(A); //O(logN)
}
for (int i = 0; i < N; ++i)
{
A[i] = tmpA[i];
}
}
- 最大堆的方式
- 定理:堆排序处理N个不同元素的随机排列的平均比较次数是 2 N log N − O ( N log log N ) 2N\log N\ -\ O(N\log \log N) 2NlogN − O(NloglogN);
- 虽然堆排序给出最佳平均时间复杂度,但实际效果不如用Sedgewick增量序列的希尔排序;
void Heap_Sort(elementType A[], int N)
{
for (int i = N / 2 - 1; i >= 0; --i)
{
PrecDown(A, i, N);
}
for (int i = N - 1; i > 0; --i)
{
Swap(&A[0], &A[i]);
PrecDown(A, 0, i);
}
}
6、归并排序
6.1、核心:有序子列的归并
- 对两个有序的子列进行归并,成为一个更大的有序子列
- 归并的时间复杂度为: T ( N ) = O ( N ) T(N)\ =\ O(N) T(N) = O(N)
- 核心程序 Merge()
void Merge (elementType A[], elementType tmpA[], int L, int R, int rightEnd)
{
//L为左边起始位置,R为右边起始位置,rightEnd为右边终点位置
int leftEnd = R - 1; //左边终点位置,假设左右两列紧挨着
int tmp = L; //结果数组的初始位置
numElements = rightEnd - L + 1;
while (L <= leftEnd && R <= rightEnd)
{
if (A[L] <= A[R]) tmpA[tmp++] = A[L++];
else tmpA[tmp++] = A[R++];
}
while (L <= leftEnd) tmpA[tmp++] = A[L++];
while (R <= rightEnd) tmpA[tmp++] = A[R++];
for (int i = 0; i < numElements; ++i, --rightEnd) A[rightEnd] = tmpA[rightEnd];
}
6.2、递归算法
- 采用递归的方式实现归并排序,该算法是稳定的
void MSort (elementType A[], elementType tmpA[], int L, int rightEnd)
{
int center;
if (L < rightEnd)
{
center = (L + rightEnd) / 2;
MSort (A, tmpA, L, center);
MSort (A, tmpA, center + 1, rightEnd);
Merge (A, tmpA, L, center + 1, rightEnd);
}
}
-
采用分而治之的方法,若整个子列的时间为 T ( N ) T(N) T(N),则左右两个子列的时间为 T ( N / 2 ) T(N/2) T(N/2);
故时间复杂度为 T ( N ) = T ( N / 2 ) + T ( N / 2 ) + O ( N ) = O ( N log N ) T(N) = T(N/2) + T(N/2) + O(N) = O(N \log N) T(N)=T(N/2)+T(N/2)+O(N)=O(NlogN) -
算法格式要求,函数名首字母大写,形参两个,一为原数据域,二为元素个数
-
统一函数接口
void Merge_sort (elementType A[], int N)
{
elementType* tmpA = new elementType[N];
if (tmpA != NULL)
{
MSort (A, tmpA, 0, N - 1);
delete[] tmpA;
}
else Error("空间不足");
}
-
声明临时数组 tmpA是在最外层进行的,如果放在 Merge中进行,将会进行多余的new操作和delete操作
-
在最外层声明
- 在Merge中声明
6.3、非递归算法
- 非递归的方式实现归并排序,是稳定的;
每每一对进行归并排序,最终分为左右子列进行归并排序,额外空间复杂度为 O ( N ) O(N) O(N)
void Merge_pass (elementType A[], elementType tmpA[], int N, int length)
{
//length为当前有序子列的长度
for (int i = 0; i <= N - 2 * length; i += 2 * length)
Merge1 (A, tmpA, i, i + length, i + 2 * length - 1);
//归并最后2个子列
if (i + length < N)
Merge1 (A, tmpA, i , i + length, N - 1); //将A中元素归并到tmpA中
//最后只剩1个子列的情况
else
for (int j = i; j < N; ++j) tmpA[j] = A[j];
}
- 统一函数接口
void Merge_sort (elementType A[], int N)
{
elementType* tmpA = new elementType[N];
if (tmpA != NULL)
{
int length = 1;
while (length < N)
{
Merge_pass (A, tmpA, N, length);
length *= 2;
Merge_pass (tmpA, A, N, length); //来回操作,并保证最后的结果是保存在A中
length *= 2;
}
delete[] tmpA;
}
else Error("空间不足");
}
7、快速排序
- 快排采取分而治之的策略,每次将子列分为左右子列;
- 快排算法最好的情况是,每次正好中分,时间复杂度为 T ( N ) = O ( N log N ) T(N)=O(N \log N) T(N)=O(NlogN)
void QuickSort (elementType A[], int N)
{
pivot = 从A[]中选一个主元;
将S = { A[] \ pivot } 分成2个独立子集:
A1 = { a in S | a <= pivot } 和
A2 = { a in S | a >= pivot };
A[] = QucikSort (A1, N1) and
{pivot} and
QuickSort (A2, N2);
}
-
选主元
主元选得好,快排才会快;
假设 p i v o t = A [ 0 ] pivot = A[0] pivot=A[0],则时间复杂度为 T ( N ) = O ( N 2 ) T(N) = O(N^2) T(N)=O(N2); -
如果使用 r a n d ( ) rand() rand()选取 p i v o t pivot pivot在时间消耗上是不划算的
-
取头、中、尾的中位数
- 也可以不局限于三个数的中位数,可以是5、7等
elementType Median3 (elementType A[], int left, int right)
{
int center = (left + right) / 2;
if (A[left] > A[center])
Swap (&A[left], &A[center]);
if (A[left] > A[right])
Swap (&A[left] > A[right]);
if (A[center] > A[right])
Swap (A[center], A[right]);
// 现在,A[left] <= A[center] <= A[right]
Swap (&A[center], &A[right - 1]); //将pivot放到右边倒数第二个位置
//只需考虑 A[left + 1] 到 A[right - 2] 的元素
return A[right - 1];
}
-
子集划分
在 A [ l e f t + 1 ] 到 A [ r i g h t − 2 ] A[left + 1]到A[right - 2] A[left+1]到A[right−2]中,假设有 i i i指向 A [ l e f t + 1 ] A[left + 1] A[left+1],有 j j j指向 A [ r i g h t − 2 ] A[right - 2] A[right−2],而主元 p i v o t = A [ r i g h t − 1 ] pivot = A[right - 1] pivot=A[right−1];- 先比较 i i i指向的元素与主元 p i v o t pivot pivot,如果比 p i v o t pivot pivot小,则 i = i + 1 i = i + 1 i=i+1向右指向下一个元素,反之若大于 p i v o t pivot pivot,则停下;
- 后比较 j j j指向的元素与主元 p i v o t pivot pivot,如果比 p i v o t pivot pivot大,则 j = j − 1 j = j - 1 j=j−1向左指向上一个元素,反之若小于 p i v o t pivot pivot,则停下;
- 当 i 与 j i与j i与j都停下时,先判断 j − i > 0 j - i > 0 j−i>0是否成立,若成立,则交换此时 i i i与 j j j指向的元素,并继续上述两步操作;反之若不成立,则跳出循环;
- 此时在循环外,交换 p i v o t pivot pivot与 i i i指向的元素,至此,主元右边的元素都大于它,主元左边的元素都小于它;
-
注意的是,当有元素正好等于 p i v o t pivot pivot,选择停下来进行交换;看起来费时间,但是这样能保证主元停在子集的中间位置;如若不这么选择,那么主元可能在两段位置,则快排的速度反而降下来;
-
小规模数据的处理
- 快速排序的问题
- 用递归方法产生的问题;
- 对小规模的数据(例如N不到100)可能好不如插入排序快;
- 解决方案
- 当递归的数据规模充分小,则停止递归,直接调用简单排序(例如插入排序);
- 在程序中定义一个 c u t o f f cutoff cutoff的阈值,阈值设置的不同,程序的效率也不同;
- 快速排序的问题
-
算法实现
void QuickSort (elementType A[], int left, int right)
{
int cutoff = 100;
if (cutoff <= (right - left))
{
elementType pivot = Median3 (A, left, right);
int i = left;
int j = right - 1;
for (;;)
{
while (A[++i] < pivot) {};
while (A[--j] > pivot) {};
if (i < j)
Swap (&A[i], &A[j]);
else break;
}
Swap (&A[i], &A[right - 1]);
QuickSort (A, left, i - 1);
QucikSort (A, i + 1, right);
}
else
Insertion_Sort (A + left, right - left + 1);
}
- 统一函数接口
void Quick_Sort (elementType A[], int N)
{
QucikSort (A, 0, N - 1);
}
8、表排序
- 间接排序
- 定义一个指针数组作为“表”(table)
排序的时候,不改变元素的位置,通过该表table的值来进行排序;
- 定义一个指针数组作为“表”(table)
如果仅要求按顺序输出,则输出:
A [ t a b l e [ 0 ] ] , A [ t a b l e [ 1 ] ] , ⋯ , A [ t a b l e [ N − 1 ] ] A[table[0]],\ A[table[1]],\ \cdots,\ A[table[N - 1]] A[table[0]], A[table[1]], ⋯, A[table[N−1]];
-
物理排序
-
N个数字的排列由若干个独立的环组成
- 可以得到 table值为3、5、1、0为一个环;2为一个环;7、4、6为一个环;
-
进行排序时,
- 先将f的元素放入Temp临时空间,并记录此时A的下标 t m p I n d e x = 0 tmpIndex = 0 tmpIndex=0,$ t a b l e = 3 指 向 A [ 3 ] = a table = 3 指向 A[3] = a table=3指向A[3]=a,将a的元素放入 A [ 0 ] A[0] A[0]的位置;
- t a b l e = 1 指 向 A [ 1 ] = d table = 1指向A[1] = d table=1指向A[1]=d,将 d的元素放入 A [ 3 ] A[3] A[3]的位置;
- t a b l e = 5 指 向 A [ 5 ] = b table = 5指向A[5] = b table=5指向A[5]=b,将 b的元素放入 A [ 1 ] A[1] A[1]的位置;
- t a b l e = 0 指 向 A [ 0 ] table = 0指向A[0] table=0指向A[0],因为满足 t a b l e = t m p I n d e x table = tmpIndex table=tmpIndex,判定当前的环已经结束了;
- 开始寻找新的环,直到没有环;
-
-
复杂度分析
- 最好情况:初始即有序
- 最坏情况:
- 由 ⌊ N / 2 ⌋ \lfloor N / 2 \rfloor ⌊N/2⌋个环,每个环包含2个元素
- 需要 ⌊ 3 N / 2 ⌋ \lfloor 3 N / 2 \rfloor ⌊3N/2⌋次元素移动
- 时间复杂度为 T = O ( m N ) T = O(mN) T=O(mN), m m m是每个 A A A元素的复制时间;
9、基数排序
- 前言
9.1、桶排序(Bucket Sort)
-
基本思路:
- 设有 K K K个桶,先扫描序列求出最大值 M a x V MaxV MaxV和最小值 M i n V MinV MinV,则把将区间 [ M i n V , M a x V ] [MinV,\ MaxV] [MinV, MaxV]均匀划分为 K K K个区间,每个区间就是一个桶;而后将序列中的元素分配到对应的桶中;
- 选择任意一种排序算法,对每个桶进行排序;
- 最后将各个桶中的元素合并成一个大的有序序列;
-
注意:
- 桶越多,时间效率就越高,但是所占空间就越大;
- 这是稳定的算法
-
假设有 N N N个学生,成绩是0到100之间的整数(于是有 M = 101 M=101 M=101个不同的成绩值),这里就可以采用桶排序
void BucketSort (elementType A[], int N)
{
count[] 初始化;
while (读入1个学生成绩grade)
将该生插入count[grade]链表;
对每条链表进行排序;
for (int i = 0; i < M; ++i)
{
//链表不为空则输出链表
if (count[i])
输出整个count[i]链表;
}
}
- 时间复杂度为 T ( N , M ) = O ( M + N ) T(N,M) = O(M + N) T(N,M)=O(M+N);
9.2、计数排序(Counting Sort)
- 基本思想:
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为 i i i的元素出现的次数,存入数组 C C C的第 i i i项;
- 对所有的计数累加(从 C C C中的第一个元素开始,每一项和前一项相加);
- 方向填充目标数组:将每个元素 i i i放在新数组的第 C ( i ) C(i) C(i)项,每放一个元素就将 C ( i ) C(i) C(i)减去1;
- 注意:
- 这是稳定的算法
9.3、基数排序(Radix Sort)
-
基本思想:
- 将整数按位数切割成不同的数字,然后按个位数分别比较;基数排序的方式可以采用 L S D ( L e a s t S i g n i f i c a n t D i g i t a l ) LSD(Least\ Significant\ Digital) LSD(Least Significant Digital)或 M S D ( M o s t S i g n i f i c a n t D i g i t a l ) MSD(Most \ Significant\ Digital) MSD(Most Significant Digital); L S D LSD LSD的排序方式由键值的最右值边开始,而 M S D MSD MSD则相反,由键值的最左边开始;
- M S D MSD MSD:先从高位开始进行排序,在每个关键字上,可采用计数排序;
- L S D LSD LSD:先从地位开始进行排序,在每个关键字上,可采用桶排序;
-
案例