定义:假设含有n 个记录的序列为
{
r
1
,
r
2
,
.
.
.
.
.
.
,
r
n
}
\{r_1,r_2, ... ... ,r_n\}
{r1,r2,......,rn}, 其相应的关键字分别为
{
k
1
,
k
2
,
.
.
.
.
.
.
,
k
n
}
\{k_1,k_2, ... ... ,k_n\}
{k1,k2,......,kn},需确定
1
,
2
,
…
…
,
n
1,2, ……, n
1,2,……,n的一种排列
p
1
,
p
2
,
.
.
.
.
.
.
,
p
n
p_1,p_2,... ...,p_n
p1,p2,......,pn,使其相应的关键字满足
k
p
1
≤
k
p
2
≤
,
.
.
.
.
.
.
,
≤
k
p
n
k_{p_1}\leq k_{p_2}\leq,... ...,\leq k_{p_n}
kp1≤kp2≤,......,≤kpn(非递减或非递增) 关系,即使得序列成为一个按关键字有序的序列
{
r
p
1
,
r
p
2
,
.
.
.
.
.
.
,
r
p
n
}
\{r_{p_1},r_{p_2},... ...,r_{p_n}\}
{rp1,rp2,......,rpn}, 这样的操作就称为排序。
稳定性:假设
k
i
=
k
j
(
1
≤
i
≤
n
,
l
≤
j
≤
n
,
i
!
=
j
)
k_i=k_j( 1\leq i\leq n,l\leq j\leq n,i!=j)
ki=kj(1≤i≤n,l≤j≤n,i!=j),且在排序前的序列中
r
i
r_i
ri领先于
r
j
(
即
i
<
j
)
r_j(即i<j)
rj(即i<j)。如果排序后仍然领先于
r
j
r_j
rj,则称所用的排序方法是稳定的;反之,若可能使得排序后的序列中
r
j
r_j
rj领先
r
i
r_i
ri,则称所用的排序方法是不稳定的。
内排序与外排序:内排序是在排序整个过程中,待排序的所有记录全部被就置在内存中。外排序是由于排序的记录个数太多, 不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。
内排序分为:插入排序、交换排序、选择排序和归并排序。
下面主要陈述两类算法:简单算法与改进算法
简单算法:冒泡排序、简单选择排序、直接插入排序。
改进算法:希尔排序、堆排序、归并排序、快速排序。
冒泡排序
算法思想:冒泡排序( Bubble Sort) 一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。第1趟冒泡会将整个序列的最小值放到序列的第1位,第二趟冒泡则是在除开第一位以外的关键字之间再进行冒泡,此时会将整个序列的次小值放置第2位,依次进行。
代码:
简单冒泡排序:
def Bubble_Sort0(L): #简单冒泡排序
for i in range(len(L)):
for j in range(i,len(L)):
if L[i]>L[j]:
L[i],L[j]=L[j],L[i]
return L
简单冒泡排序并不是一个好的算法(其实不能称作冒泡排序),因为在它排序的过程中,会出现因为是与前面所有元素比较再考虑是否交换顺序,而有可能导致在原本序列中位置较靠前的较小的关键字(即原本序列中关键字的位置与排序后该关键字应该的位置较相似)给换到了靠后的位置,这反而增加了后续交换的复杂度。
正宗冒泡排序:
def Bubble_Sort1(L): #冒泡排序
for i in range(len(L)):
j=len(L)-1
while(j>i):
if L[j-1]>L[j]:
L[j-1],L[j]=L[j],L[j-1]
j-=1
return L
正宗的冒泡排序可以保证,每进行一趟冒泡,即可找出
L
[
i
:
]
L[i:]
L[i:]中的最小值,并且将之变到
L
[
i
]
L[i]
L[i]处,并且进行的是相邻关键字的交换,因为在交换前后,关键字的位置大抵是不变的。
但是冒泡算法对于这种情况的处理方式是很糟糕的:
{
2
,
1
,
3
,
4
,
5
,
6
,
7
,
8
,
9
}
\{2,1,3,4,5,6,7,8,9\}
{2,1,3,4,5,6,7,8,9},我们可以从代码中得知,在进行了第一趟冒泡后,便已经排好了序,但是代码却要继续进行下去,此时浪费了许多时间,因此可以考虑对原本的算法进行改进,添加一个标记,来判断是否应该继续排序。
改进冒泡排序:
设置一个用于判断的变量
f
r
a
g
frag
frag当
f
r
a
g
=
T
r
u
e
{frag=True}
frag=True时,代表此次冒泡进行了交换,当
f
r
a
g
=
F
a
l
s
e
{frag=False}
frag=False代表此次冒泡没有进行任何操作,这说明序列已经有序,故不用再继续排序。
def Bubble_Sort2(L): #改进冒泡排序
flag=True
for i in range(len(L)):
if flag==False:
break
flag=False
j=len(L)-1
while(j>i):
if L[j-1]>L[j]:
L[j-1],L[j]=L[j],L[j-1]
flag=True
j-=1
return L
复杂度分析
时间复杂度:如果考虑改进后的算法,最好的情况是本身就有序,那么需要比较的次数为
n
−
1
n-1
n−1,没有数据交换,故时间复杂度为
O
(
n
)
O(n)
O(n);如果考虑最坏的情况,即逆序,此时需要比较的次数为
n
(
n
−
1
)
2
\frac{n(n-1)}{2}
2n(n−1),移动的次数也为
n
(
n
−
1
)
2
\frac{n(n-1)}{2}
2n(n−1),故时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。平均时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。
空间复杂度:因为只需要一个额外的储存空间来储存待交换的元素,故空间复杂度为
O
(
1
)
O(1)
O(1)。
稳定性:对于
B
u
b
b
l
e
_
S
o
r
t
0
Bubble\_Sort0
Bubble_Sort0而言,因为比较和交换都是跳跃的,故不稳定;对于
B
u
b
b
l
e
_
S
o
r
t
1
Bubble\_Sort1
Bubble_Sort1与
B
u
b
b
l
e
_
S
o
r
t
2
Bubble\_Sort2
Bubble_Sort2而言,因为比较和交换均为相邻的,故稳定。
简单选择排序
算法思想:简单选择排序法 ( S i m p l e S e l e c t i o n S o r t ) (Simple Selection Sort) (SimpleSelectionSort)就是通过 n − i n-i n−i次关键字间的比较,从 n − j + 1 n-j+1 n−j+1个记录中选出关键字最小的记录,并和第 i ( 1 < i < n ) i(1<i<n) i(1<i<n)个记录交换之。第1次排序选出最小值放至第1位,第2次选出次小值放至第2位,依次进行。
代码
def Simple_Selection_Sort(L): #简单选择排序
for i in range(len(L)):
Min=i
for j in range(i+1,len(L)): #在L[i+1:]中寻找比L[i]小的值中的最小值
if L[Min]>L[j]:
Min=j
if i!=Min:
L[Min],L[i]=L[i],L[Min]
return L
复杂度分析
时间复杂度:简单选择排序最大的特点就是交换移动数据的次数较少。比较次数为
n
(
n
−
1
)
2
\frac{n(n-1)}{2}
2n(n−1),交换次数最好的时候为0次,最差的时候为
n
−
1
{n-1}
n−1次。最终简单选择排序的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。虽然与冒泡排序的时间复杂度相同,但是性能要略优于冒泡排序。
空间复杂度:
O
(
1
)
O(1)
O(1)。
稳定性:考虑到交换的具体情况,故稳定。
直接插入排序
算法思想:直接插入排序 ( S t r a i g h t I n s e r t i o n S o r t ) {(Straight Insertion Sort)} (StraightInsertionSort)的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
代码
def Straight_Insertion_Sort(L):
for i in range(1,len(L)):
temp=L[i]
j=i-1
while(j>=0 and L[j]>temp):
L[j+1]=L[j]
j-=1
L[j+1]=temp
return L
复杂度分析
时间复杂度:考虑最好的情况,本身就有序时,代码循环中进行的部分只有
L
[
j
]
L[j]
L[j]与
t
e
m
p
temp
temp的比较,一共比较
n
−
1
n-1
n−1次,没有交换,故时间复杂度为
O
(
n
)
O(n)
O(n);考虑最坏的情况,排序表是逆序时,需要比较
n
(
n
−
1
)
2
\frac{n(n-1)}{2}
2n(n−1)次,移动的次数也达到最大
(
n
+
2
)
(
n
−
1
)
2
\frac{(n+2)(n-1)}{2}
2(n+2)(n−1),故此时,时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。如果排序记录是随机的,那么根据概率相同的原则,平均比较和移动次数约为
n
2
4
\frac{n^2}{4}
4n2次,故最终的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。直接插入排序比冒泡排序和简单选择排序性能要好一些。
空间复杂度:其空间复杂度为
O
(
1
)
O(1)
O(1)。
稳定性:稳定
希尔排序
已知直接插入排序在序列基本有序或者在关键字较少时,效率很高,因此可以考虑先将原始序列排列成基本有序的序列,然后再进行直接插入排序。那么希尔排序的实现便是构造基本有序序列的过程。希尔排序的关键并不是随便分组后各自排序,而是将相隔某个“增量”的记录组成一个子序列, 实现跳跃式的移动,使得排序的效率提高。
算法思想:希尔排序是把关键字按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键字越来越多,当增量减至1时,整个序列恰被分成一组,算法便终止。
代码:
def Shell_Sort(L):
increment=len(L)
while(increment>1):
increment=int(increment/3)+1
for i in range(increment,len(L)):
if L[i]<L[i-increment]:
temp=L[i]
j=i-increment
while(j>=0 and L[j]>temp):
L[j+increment]=L[j]
j-=increment
L[j+increment]=temp
return L
复杂度分析
时间复杂度:时间复杂度与代码中的“增量”:
i
n
c
r
e
m
e
n
t
increment
increment有关,应该选取什么样的增量才是最好,目前还是一个数学难题,迄今为止还没有人找到一种最好的增量序列。不过大量的研究表明,当增量序列为
d
l
t
a
[
k
]
=
2
t
−
k
+
1
−
1
(
0
≤
k
≤
t
≤
⌊
log
2
(
n
+
1
)
⌋
)
{dlta[k]=2^{t-k+1}-1(0\leq k\leq t\leq \lfloor \log_2(n+1)\rfloor)}
dlta[k]=2t−k+1−1(0≤k≤t≤⌊log2(n+1)⌋)时,可以获得不错的效果,其时间复杂度为
O
(
n
3
2
)
O(n^{\frac{3}{2}})
O(n23)。最好情况:
O
(
n
1.3
)
O(n^{1.3})
O(n1.3),最坏情况:
O
(
n
2
)
O(n^2)
O(n2),平均情况:
O
(
n
log
n
)
至
O
(
n
2
)
{O(n\log n)至O(n^2)}
O(nlogn)至O(n2)
空间复杂度:就地排序,故空间复杂度为
O
(
1
)
O(1)
O(1)。
稳定性:因为其进行了划分以及跳跃比较与交换,故不稳定
堆排序
堆
(
H
e
a
p
)
(Heap)
(Heap):堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
算法思想:堆排序
(
H
e
a
p
S
o
r
t
)
(Heap\ Sort)
(Heap Sort)就是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值)。然后将剩余的
n
−
1
n-1
n−1个序列重新构造成一个堆,这样就会得到
n
n
n个元素中的次小值。如此反复执行,便能得到一个有序序列了。算法得关键便是给任意一组数据,怎么构成堆,再原本是堆得数组中,将首位元素互换后,又怎么将剩下得前
n
−
1
n-1
n−1个数调整成一个堆。
代码
def Heap_Adjust(L,m,n):
temp=L[m]
i=m
j=2*i+1
while(j<=n):
if j<n and L[j]<L[j+1]:
j+=1
if temp<L[j]:
L[i],i=L[j],j
j=2*i+1
else:
break
L[i]=temp
def Heap_Sort(L):
L_half=int((len(L)+1)/2)
for i in range(L_half): #第一个for循环,用于生成堆
Heap_Adjust(L,L_half-i-1,len(L)-1)
print(L)
for i in range(len(L)-1): #第二个for循环用于调序
L[0],L[-i-1]=L[-i-1],L[0]
Heap_Adjust(L,0,len(L)-i-2)
return L
Note:
1.函数
d
e
f
H
e
a
p
_
A
d
j
u
s
t
(
L
,
m
,
n
)
{def\ Heap\_Adjust(L,m,n)}
def Heap_Adjust(L,m,n)用于调整序列
L
L
L,改变
t
e
m
p
=
L
[
m
]
{temp=L[m]}
temp=L[m]在
L
L
L中的位置(不妨设为在
L
[
k
]
L[k]
L[k]处,在调整
t
e
m
p
temp
temp的位置期间,会伴随着其他位置的关键字的移动),满足:使得
t
e
m
p
temp
temp变得比它的根节点小,
t
e
m
p
temp
temp的根节点比
t
e
m
p
temp
temp的根节点的根节点小……
(
即
L
[
k
]
<
L
[
⌊
(
k
−
1
)
/
2
⌋
]
<
L
[
⌊
(
(
k
−
1
)
/
2
−
1
)
/
2
⌋
]
…
…
(即{L[k]<L[\lfloor(k-1)/2\rfloor]<L[\lfloor((k-1)/2-1)/2\rfloor]……}
(即L[k]<L[⌊(k−1)/2⌋]<L[⌊((k−1)/2−1)/2⌋]……)。
(注:原本对于完全二叉树,第
n
n
n个节点的双亲节点(若存在)为
⌊
n
/
2
⌋
\lfloor n/2\rfloor
⌊n/2⌋,左孩子(若存在)为
2
n
2n
2n,右孩子(若存在)为
2
n
+
1
2n+1
2n+1,但是列表
L
L
L是从0开始的,所以第
n
n
n个节点的双亲节点(若存在)为
⌊
(
n
−
1
)
/
2
⌋
\lfloor (n-1)/2\rfloor
⌊(n−1)/2⌋,左孩子(若存在)为
2
n
+
1
2n+1
2n+1,右孩子(若存在)为
2
n
+
2
2n+2
2n+2。)
2.函数
d
e
f
H
e
a
p
_
S
o
r
t
(
L
)
def\ Heap\_Sort(L)
def Heap_Sort(L)中的第1个
f
o
r
for
for循环,是为了对
L
L
L创建堆,需要注意的是由堆的定义可知,我们并不需要对L每个位置进行操作,只需要对
{
L
[
0
]
,
L
[
1
]
,
…
…
,
L
[
⌈
L
_
l
e
n
g
t
h
/
2
⌉
−
1
]
}
\{L[0],L[1],……,L[\lceil L\_length/2 \rceil -1]\}
{L[0],L[1],……,L[⌈L_length/2⌉−1]} 这
⌈
L
_
l
e
n
g
t
h
/
2
⌉
{\lceil L\_length/2 \rceil}
⌈L_length/2⌉个位置进行操作即可(这是因为只有这些位置才有孩子)。
3.函数
d
e
f
H
e
a
p
_
S
o
r
t
(
L
)
def\ Heap\_Sort(L)
def Heap_Sort(L)中的第2个
f
o
r
for
for循环,因为在第1个
f
o
r
for
for循环中已经使得
L
L
L成为了一个大顶堆,在交换了
L
[
0
]
,
L
[
−
i
−
1
]
L[0],L[-i-1]
L[0],L[−i−1]后,对于
L
[
0
:
(
L
_
l
e
n
g
t
h
−
1
)
−
(
i
+
1
)
]
{L[0:(L\_length-1)-(i+1)]}
L[0:(L_length−1)−(i+1)],只有位置
L
[
0
]
L[0]
L[0]不满足堆的定义,故只需使用函数
H
e
a
p
_
A
d
j
u
s
t
Heap\_Adjust
Heap_Adjust一次后,
L
[
0
:
(
L
_
l
e
n
g
t
h
−
1
)
−
(
i
+
1
)
]
{L[0:(L\_length-1)-(i+1)]}
L[0:(L_length−1)−(i+1)]便又成为了一个大顶堆。
复杂度分析
时间复杂度:在正式排序时,第
i
i
i次取堆顶记录重建堆需要用
O
(
log
i
)
O(\log i)
O(logi)的时间(完全二叉树的某个结点到根结点的距离为
⌊
log
2
i
⌋
+
1
\lfloor \log_2i\rfloor +1
⌊log2i⌋+1) ,并且需要取
n
−
1
n-1
n−1次堆顶记录,所以重建堆的时间复杂度为
O
(
n
log
n
)
O(n\log n)
O(nlogn),由于堆排序对原始记录的排序状态并不敏感,因此它无论是最好、最坏和平均时间复杂度均为
O
(
n
log
n
)
O(n\log n)
O(nlogn)。
空间复杂度:就地排序,故为
O
(
1
)
O(1)
O(1)。
稳定性:不稳定
归并排序
算法原理:归并排序 ( M e r g i n g S o r t ) (Merging\ Sort) (Merging Sort)就是利用归并的思想实现的排序方法。它的原理是假设初始序列含有 n n n个记录,则可以看成是 n n n个有序的子序列,每个子序列的长度为1,然后两两归并,得到 ⌈ n / 2 ⌉ \lceil n/2\rceil ⌈n/2⌉个长度为2或1的有序子序列;再两两归并,……,如此重复, 直至得到一个长度为 n n n的有序序列为止,这种排序方法称为2路归并排序。
代码
def MSort(L): #归并
L1=L[0:int(len(L)/2)]
L2=L[int(len(L)/2):]
if len(L)>1:
L1=MSort(L1)
L2=MSort(L2)
L=Merge(L1,L2)
return L
def Merge(L1,L2):
i,j=0,0
L=[]
while(i<len(L1) and j<len(L2)):
if L1[i]<L2[j]:
L.append(L1[i])
i+=1
else:
L.append(L2[j])
j+=1
if i==len(L1):
L.extend(L2[j:])
else:
L.extend(L1[i:])
return L
Note:真正的排序代码存在于函数 M e r g e Merge Merge中,即当 L 1 L_1 L1与 L 2 L_2 L2合并时,会进行比较排序。
复杂度分析
时间复杂度:最好、最坏、平均的时间性能相同,即
O
(
n
log
n
)
O(n\log n)
O(nlogn)
空间复杂度:由于归并排序在归并过程中需要与原始记录序列同样数量的存储空间存放归并结果以及递归时深度为
log
n
\log n
logn的栈空间,因此空间复杂度为
O
(
n
+
log
n
)
=
O
(
n
)
O(n+\log n)=O(n)
O(n+logn)=O(n)。
稳定性:因为归并中不存在跳跃,故稳定。
归并排序优化
对于归并排序是使用递归函数来实现的,但是也可以使用
w
h
i
l
e
while
while循环为来实现,如果使用
w
h
i
l
e
while
while循环实现的话,会缩减栈的深度,避免了深度为
log
n
\log n
logn的栈空间,此时的空间复杂度为
O
(
n
)
O(n)
O(n)。避免递归也在时间性能上有所提升。
代码:
def Merge(L1,L2):
i,j=0,0
L=[]
while(i<len(L1) and j<len(L2)):
if L1[i]<L2[j]:
L.append(L1[i])
i+=1
else:
L.append(L2[j])
j+=1
if i==len(L1):
L.extend(L2[j:])
else:
L.extend(L1[i:])
return L
def MSort2(L):
K=[[i] for i in L]
while len(K)>1:
i=0
while i+1<len(K):
K[i]=Merge(K[i],K[i+1])
del K[i+1]
i+=1
return K[0]
快速排序
算法思想:快速排序
(
Q
u
i
c
k
S
o
r
t
)
(Quick\ Sort)
(Quick Sort)的基本思想是通过一趟排序将待排序列分割成独立的两部分,其中一部分的关键字均比另一部分的关键字小,则可分别对这两部分继续进行排序,以达到整个序列有序的目的。
代码
def QuickSort(L):
Min=[]
Max=[]
Equal=[]
if L:
pivot=L[0]
for x in L:
if x>pivot:
Max.append(x)
elif x<pivot:
Min.append(x)
else:
Equal.append(x)
return QuickSort(Min)+Equal+QuickSort(Max)
else:
return L
def quicksort(L):
if L:
i,j=0,len(L)-1
while(i<j):
while(i<j and L[i]<=L[j]):
j-=1
L[i],L[j]=L[j],L[i]
while(i<j and L[j]>=L[i]):
i+=1
L[i],L[j]=L[j],L[i]
return quicksort(L[0:i])+[L[i]]+quicksort(L[i+1:])
else:
return L
复杂度分析
时间复杂度:在最优的情况下,即每次都分的很均匀的情况下,快速排序的时间复杂度为
O
(
n
log
n
)
O(n\log n)
O(nlogn);在最坏的情况下,即序列原本便为正序或者逆序的情况下,每次仅划分一个元素,此时的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2);平均情况下的时间复杂度为
O
(
n
log
n
)
O(n\log n)
O(nlogn)。
空间复杂度:因为此处所给出的快速排序算法并不是就地排序,每次层递归都需要额外的储存空间为
n
n
n,而对于递归深度,最好的情况是
log
n
\log n
logn,而最坏的情况需要
n
n
n层递归调用函数,总的来说,空间复杂度为
O
(
n
)
O(n)
O(n)。对于一般情况,空间复杂度为
O
(
log
n
)
至
O
(
n
)
O(\log n)至O(n)
O(logn)至O(n)。
稳定性:因为关键字的比较和交换是跳跃的,故不稳定。
快速排序优化
1.可以优化选取枢轴:
p
i
v
o
t
pivot
pivot,当
p
i
v
o
t
pivot
pivot取值较为适中时,划分的情况会比较好,但是当
p
i
v
o
t
pivot
pivot取到最大值或者最小值时,划分的情况会很糟糕。因此就有了三数取中法
(
m
e
d
i
a
n
−
o
f
−
t
h
r
e
e
)
(median-of-three)
(median−of−three):即取三个关键字先进行排序,将中间数作为枢轴, 一般是取左端、右端和中间三个数,也可以随机选取。当然还可以有九数取中法
(
m
e
d
i
a
n
−
o
f
−
n
i
n
e
)
(median-of-nine)
(median−of−nine)。
2.优化不必要的交换。此处用
p
y
t
h
o
n
python
python写出的快速排序可以很自然地避免掉交换这一过程。
3.优化短序列时的排序方案。当序列长度较小时,直接使用直接插入排序,长度较大时再使用快速排序。
4.减少递归操作。
P.S.非基于比较的排序算法——计数排序
算法思想:对于序列中每个关键字x,通过记录序列中比x小的关键字个数来得到x在排序好后的序列中的位置,通过记录与x相同的关键字个数来得到x所在区间。
代码
def Count_Sort(L):
n=len(L)
K=[None]*n
for i in range(n):
p=0
q=0
for j in range(n):
if L[j]<L[i]: #记录比L[i]小的元素个数
p+=1
elif L[j]==L[i]: #记录与L[i]相等的元素个数
q+=1
for k in range(p,p+q):
K[k]=L[i]
print(K)
复杂度分析
时间复杂度:假定输入这个算法的序列关键字均为
0
0
0~
k
k
k的整数,序列长度为
n
n
n,则其时间复杂度可以达到
O
(
n
+
k
)
O(n+k)
O(n+k)。这是一种牺牲空间换取时间的算法,当
O
(
k
)
>
O
(
n
log
n
)
{O(k)>O(n\log n)}
O(k)>O(nlogn)的时候其效率反而不如基于比较的排序(因为基于比较的排序算法理论下限是
O
(
n
log
n
)
O(n\log n)
O(nlogn))。
空间复杂度:
O
(
n
)
O(n)
O(n)。
稳定性:稳定