作为数据结构的课程笔记,以便查阅。如有出错的地方,还请多多指正!
目录
冒泡排序 Bubble Sort
排序过程
思路:
- 比较
1st
与2nd
记录的关键字,若逆序则交换;然后比较2nd
与3rd
记录的关键字;……直至比较n-1th
与nth
记录为止。完成第一趟冒泡排序,则关键字最大的记录被放置在最后一个位置上 - 对前
n-1
个记录进行第二趟冒泡排序,将关键字次大的记录放置在倒数第二个位置上 - 重复上述过程,直到“在一趟排序过程中没有进行过交换记录的操作”为止
算法实现
void Bubble_sort(SqList_t* list)
{
for (int i = 0; i < list->len - 1; ++i)
{
int flag = 0;
for (int j = 1; j < list->len - i; ++j)
{
if (list->rec[j].key > list->rec[j + 1].key)
{
list->rec[0] = list->rec[j];
list->rec[j] = list->rec[j + 1];
list->rec[j + 1] = list->rec[0];
flag = 1;
}
}
if (0 == flag)
{
break;
}
}
}
算法评价
适用于元素较少,或初始序列基本有序
T(n)
冒泡排序最多执行 n − 1 n-1 n−1趟。第 i i i趟排序需要比较 n − i n-i n−i次
若待排记录为从小到大排列(正序)
- 比较次数: n − 1 n-1 n−1
- 移动次数: 0 0 0
若待排记录为从大到小排列
- 比较次数: ∑ i = 1 n − 1 ( n − i ) = n 2 − n 2 \sum_{i=1}^{n-1} (n-i) = \frac {n^2-n}{2} ∑i=1n−1(n−i)=2n2−n
- 移动次数: 3 ∑ i = 1 n − 1 ( n − i ) = 3 ( n 2 − n ) 2 3\sum_{i=1}^{n-1} (n-i)= \frac {3(n^2-n)}{2} 3∑i=1n−1(n−i)=23(n2−n)
∴ T ( n ) = O ( n 2 ) \therefore T(n)=O(n^2) ∴T(n)=O(n2)
S(n)
S ( n ) = O ( 1 ) S(n)=O(1) S(n)=O(1)
是否稳定
- 稳定
快速排序 Quick Sort
排序过程
思路
- 在待排序记录中任取一个记录 (通常为第一个记录) ,作为枢轴(pivot),将其它记录分为两个子序列:
- 子序列 1 中,记录的关键值 < < < 枢轴的关键值
- 子序列 2 中,记录的关键值 ≥ \geq ≥ 枢轴的关键值
- 将枢轴放置在两个子序列之间 (即枢轴的最终位置),完成一趟快速排序
- 分别递归地对两个子序列重复上述过程,直到序列有序
一趟快排的具体实现过程:
- 设置标志
low
和high
,初始指向待排序记录的首尾 - 从
high
开始向前搜索,直到找到一个关键值<pivot
的记录,将其放置在low
所指位置,low
后移 - 从
low
开始向后搜索,直到找到一个关键值>=pivot
的记录,将其放置在high
所指位置,high
前移 - 重复上面两步,直到
low
==high
,将pivot
放置在该位置,一趟快速排序结束
一趟快排结束,接下来递归地对左右子序列进行快排即可:
算法实现
int Partiton(SqList_t* list, int low, int high)
{
list->rec[0] = list->rec[low]; //选择枢轴
while (low < high)
{
while (high > low && list->rec[high].key >= list->rec[0].key)
{
--high;
}
list->rec[low] = list->rec[high];
while (high > low && list->rec[low].key <= list->rec[0].key)
{
++low;
}
list->rec[high] = list->rec[low];
}
list->rec[low] = list->rec[0];
return low; //返回枢轴位置
}
void Quick_sort_reccurent(SqList_t* list, int low, int high)
{
if (low < high)
{
int pivot_loc = Partiton(list, low, high);
Quick_sort_reccurent(list, low, pivot_loc - 1);
Quick_sort_reccurent(list, pivot_loc + 1, high);
}
}
void Quick_sort(SqList_t* list)
{
Quick_sort_reccurent(list, 1, list->len);
}
算法评价
T ( n ) T(n) T(n)
最坏情况 1:初始序列为升序,每次总选到最小元素作枢轴
- 需要 n − 1 n-1 n−1 趟快排,每趟比较 n − i n-i n−i 次
- 比较次数:
∑
i
=
1
n
−
1
(
n
−
i
)
=
n
2
−
n
2
\sum_{i=1}^{n-1} (n-i) = \frac {n^2-n}{2}
∑i=1n−1(n−i)=2n2−n
∴ T ( n ) = O ( n 2 ) \therefore T(n)=O(n^2) ∴T(n)=O(n2)
最坏情况2:初始序列为降序,每次总选到最大元素作枢轴
- 需要 n − 1 n-1 n−1趟快排,每趟比较 n − i n-i n−i次,移动1次
- 比较次数: ∑ i = 1 n − 1 ( n − i ) = n 2 − n 2 \sum_{i=1}^{n-1} (n-i) = \frac {n^2-n}{2} ∑i=1n−1(n−i)=2n2−n
- 移动次数:
n
−
1
n-1
n−1
∴ T ( n ) = O ( n 2 ) \therefore T(n)=O(n^2) ∴T(n)=O(n2)
最好情况:每次总是选到中间值作枢轴
递归算法的时间复杂度分析:
设问题规模为
n
n
n,利用分治法得到
a
a
a个规模为
n
/
b
n/b
n/b的问题,每次递归带来的额外计算为
c
∗
(
n
d
)
c*(n^d)
c∗(nd)
即:
T
(
n
)
=
a
T
(
n
/
b
)
+
c
(
n
d
)
T(n)=aT(n/b)+c(n^d)
T(n)=aT(n/b)+c(nd)
则:
- 若 a = b d , T ( n ) = O ( n d l o g 2 n ) a=b^d,T(n)=O(n^d log_2n) a=bd,T(n)=O(ndlog2n)
- 若 a < b d , T ( n ) = O ( n d ) a<b^d,T(n)=O(n^d) a<bd,T(n)=O(nd)
- 若 a > b d , T ( n ) = O ( n l o g b a ) a>b^d,T(n)=O(n^{log_ba}) a>bd,T(n)=O(nlogba)
最好情况下,
a
=
2
,
b
=
2
,
d
=
1
a=2,b=2,d=1
a=2,b=2,d=1,有
a
=
b
d
a=b^d
a=bd
∴
T
(
n
)
=
O
(
n
d
l
o
g
2
n
)
=
O
(
n
l
o
g
2
n
)
\therefore T(n)=O(n^d log_2n)=O(n log_2n)
∴T(n)=O(ndlog2n)=O(nlog2n)
快排平均时间复杂度计算
- 设
T
p
a
s
s
(
n
)
=
c
n
T_{pass}(n)=cn
Tpass(n)=cn 为对
n
n
n 个记录进行一趟快排所需时间
T ( n ) = T p a s s ( n ) + T ( k − 1 ) + T ( n − k ) T(n)=T_{pass}(n)+T(k-1)+T(n-k) T(n)=Tpass(n)+T(k−1)+T(n−k)因为待排记录是随机排列的,则在一趟排序之后, k k k 取 1 1 1 ~ n n n 之间任何一值的概率相同
∴ T a v g ( n ) = c n + 1 n ∑ k = 1 n ( T a v g ( k − 1 ) + T a v g ( n − k ) ) = c n + 2 n ∑ i = 0 n − 1 T a v g ( i ) ∴ ∑ i = 0 n − 1 T a v g ( i ) = n 2 T a v g ( n ) − c n 2 2 ( 1 ) 式 ∵ ∑ i = 0 n − 1 T a v g ( i ) = ∑ i = 0 n − 2 T a v g ( i ) + T a v g ( n − 1 ) = n − 1 2 T a v g ( n − 1 ) − c ( n − 1 ) 2 2 + T a v g ( n − 1 ) ( 2 ) 式 由 ( 1 ) 式 = ( 2 ) 式 可 得 : T a v g ( n ) = n + 1 n T a v g ( n − 1 ) + c ( 2 n − 1 ) n ≈ n + 1 n T a v g ( n − 1 ) + 2 c ( 省 略 了 一 个 比 常 数 项 还 小 的 数 ) ∴ T a v g ( n ) n + 1 = T a v g ( n − 1 ) n + 2 c n + 1 现 在 可 以 进 行 叠 缩 : T a v g ( n − 1 ) n = T a v g ( n − 2 ) n − 1 + 2 c n T a v g ( n − 2 ) n − 1 = T a v g ( n − 3 ) n − 2 + 2 c n − 1 ⋮ T a v g ( 2 ) 3 = T a v g ( 1 ) 2 + 2 c 3 将 上 面 n − 1 个 式 子 相 加 : T a v g ( n ) n + 1 = T a v g ( 1 ) 2 + 2 c ( ∑ i = 3 n + 1 1 i ) ≈ T a v g ( 1 ) 2 + 2 c ( ln ( n − 1 ) + γ − 3 2 ) ∴ T a v g ( n ) ≈ 2 c ( n + 1 ) ln ( n − 1 ) + d = O ( n log n ) \begin{aligned} \therefore T_{avg}(n)&=cn+\frac{1}{n}\sum_{k=1}^n(T_{avg}(k-1)+T_{avg}(n-k)) \\&= cn+\frac{2}{n}\sum_{i=0}^{n-1}T_{avg}(i) \\ \therefore \sum_{i=0}^{n-1}T_{avg}(i) &= \frac{n}{2}T _{avg}(n) - \frac{cn^2}{2} \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ (1)式 \\\because \sum_{i=0}^{n-1}T_{avg}(i) &= \sum_{i=0}^{n-2}T_{avg}(i) + T_{avg}(n-1) \\&= \frac{n-1}{2}T _{avg}(n-1) - \frac{c(n-1)^2}{2} + T_{avg}(n-1) \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ (2)式 \\由(1)式=(2)&式可得:\\ T_{avg}(n)&= \frac{n+1}{n}T _{avg}(n-1) + \frac{c(2n-1)}{n}\approx\frac{n+1}{n}T _{avg}(n-1) + 2c\ \ \ \ \ (省略了一个比常数项还小的数) \\\therefore \frac{T_{avg}(n)}{n+1}&=\frac{T_{avg}(n-1)}{n}+\frac{2c}{n+1}\\ 现在可以进行&叠缩:\\ \frac{T_{avg}(n-1)}{n}&=\frac{T_{avg}(n-2)}{n-1}+\frac{2c}{n} \\ \frac{T_{avg}(n-2)}{n-1}&=\frac{T_{avg}(n-3)}{n-2}+\frac{2c}{n-1}\\ \vdots \\ \frac{T_{avg}(2)}{3}&=\frac{T_{avg}(1)}{2}+\frac{2c}{3}\\ 将上面n-1个&式子相加:\\ \frac{T_{avg}(n)}{n+1}&=\frac{T_{avg}(1)}{2}+2c(\sum_{i=3}^{n+1}\frac{1}{i})\\ &\approx\frac{T_{avg}(1)}{2}+2c(\ln(n-1)+\gamma-\frac{3}{2}) \\\therefore T_{avg}(n)&\approx 2c(n+1)\ln(n-1)+d=O(n\log n) \end{aligned} ∴Tavg(n)∴i=0∑n−1Tavg(i)∵i=0∑n−1Tavg(i)由(1)式=(2)Tavg(n)∴n+1Tavg(n)现在可以进行nTavg(n−1)n−1Tavg(n−2)⋮3Tavg(2)将上面n−1个n+1Tavg(n)∴Tavg(n)=cn+n1k=1∑n(Tavg(k−1)+Tavg(n−k))=cn+n2i=0∑n−1Tavg(i)=2nTavg(n)−2cn2 (1)式=i=0∑n−2Tavg(i)+Tavg(n−1)=2n−1Tavg(n−1)−2c(n−1)2+Tavg(n−1) (2)式式可得:=nn+1Tavg(n−1)+nc(2n−1)≈nn+1Tavg(n−1)+2c (省略了一个比常数项还小的数)=nTavg(n−1)+n+12c叠缩:=n−1Tavg(n−2)+n2c=n−2Tavg(n−3)+n−12c=2Tavg(1)+32c式子相加:=2Tavg(1)+2c(i=3∑n+1i1)≈2Tavg(1)+2c(ln(n−1)+γ−23)≈2c(n+1)ln(n−1)+d=O(nlogn)其中 γ ≈ 0.577 \gamma\approx0.577 γ≈0.577 为欧拉常数,详见调和级数 - 快速排序的平均时间为 T a v g ( n ) = O ( n log n ) T_{avg}(n)=O(n\log n) Tavg(n)=O(nlogn),在所有同数量级 O ( n log n ) O(n\log n) O(nlogn) 的排序方法中,它的常数因子最小。但是初始序列基本有序时,快速排序将退化为 O ( n 2 ) O(n^2) O(n2)
改进
- 改进:比较
R[low]
,R[high]
,R[(low+high)/2]
,取三个关键字中取中值的记录为枢轴,将其与R[low]
互换。算法实现不变. 然而,即使这样改进之后,也不能使快排在待排序列基本有序时达到 O ( n ) O(n) O(n) 复杂度,可进一步修改为“一次划分”算法来改善平均性能:- 在
low
加 1 和high
减 1 的同时进行冒泡操作,即在相邻两个记录处于逆序时互换 - 如果
low
从低端向中间移动过程中没有进行过交换,则不需要对低端子表进行排序 - 如果
high
从高端向中间移动过程中没有进行过交换,则不需要对高端子表进行排序
- 在
S ( n ) S(n) S(n)
- 递归函数需要额外的栈空间
- 最坏情况:初始序列为升/降序
S ( n ) = O ( n ) S(n)=O(n) S(n)=O(n) - 最好情况:每次总是选到中间值作枢轴,栈的最大深度为
⌊
l
o
g
2
n
⌋
+
1
\lfloor log_2n\rfloor+1
⌊log2n⌋+1
S ( n ) = O ( log 2 n ) S(n)=O(\log_2n) S(n)=O(log2n)
- 最坏情况:初始序列为升/降序
是否稳定
- 不稳定
- 例如:
3 2 2*
->2* 3 3
- 例如:
总结
- 就平均时间而言,快速排序算法被认为是内部排序方法中最好的一种
- 对于小规模数据,不适合选用快速排序