简单排序
插入排序
想象一下插队的过程...
一个人通过插队的方式排到前面,而将原本在他前面的人挤到了后面的位置。
对数排序时也是这样,为了从小到大排序,需要将一个数放到前面,而将那些比它大的数挤到了后面,从而实现了排序的目的。
排序过程
- 当一个数列A开始进行排序时就已经被划分成了两个部分--有序的和无序的,由于只有一个数的数列可以被看作已经有序,所以将数列A中的第一个元素看作有序的部分(图中6),后面的其他数看作无序的部分(图中5,3,1,8,7,2,4)
- 从前到后扫描有序的部分,将无序部分中的第一个元素插入到有序的部分中合适的位置(3插到5,6的前面)
- 然后将有序序列中该数后面的数依次后移一位(5,6后移一位),形成新的有序部分(3, 5, 6)
- 重复直到遍历完整个数列。
效率分析
空间复杂度
O(n)用于存储整个数列,O(1)辅助,暂存操作数,用于插入。
时间复杂度
- 最好:已经有序的情况下,只需要遍历数列一次,
为O(n)
。 - 最坏:反序情况下比较次数依次是
1 + 2 + 3 + ... + (N - 1)
,即(1/2)n(n-1)
次。O(n^2)
。 - 平均:
O(n^2)
算法实现
根据上述的排序过程,易用数组进行实现,这里不再赘述。但使用链表实现中每次后移的过程会大大降低排序的效率,以下两种实现可以
C语言链表实现
struct LIST * InsertionSort(struct LIST * pList)
{
if(pList == NULL || pList->pNext == NULL) {
return pList;
}
struct LIST * head = NULL; // head为有序部分的第一个元素
while(pList != NULL) {
struct LIST * current = pList;
pList = pList->pNext;
if(head == NULL || current->iValue < head->iValue) {
// 插入有序部分的第一个位置
current->pNext = head;
head = current;
} else {
// 插入有序部分的中间位置
struct LIST * p = head;
while(p != NULL) {
if(p->pNext == NULL || current->iValue < p->pNext->iValue) {
current->pNext = p->pNext;
p->pNext = current;
break;
}
p = p->pNext;
}
}
}
return head;
}
Python实现
采用从后向前遍历插入的方法,并利用Python中的切片,每次插入一个数之后不必再使用后移的方式调整有序序列的位置,而是直接拼接切片并返回。
def insertion_sort(alist):
length = len(alist)
if length == 1:
return alist
b = insertion_sort(alist[1:])
for index in range(len(b)):
if alist[0] <= b[index]:
return b[:index] + [alist[0]] + b[index:]
return b + [alist[0]]
选择排序
这次不再将最小的数放到最前面,让后面的数自动往后面移位,而是每次选择出最小的数,和排在第一个的数进行交换。从而达到将较小的数排在前面的目的。
排序过程
以从小到大排序为例:
- 在未排序序列中找到最小元素,存放到排序序列的起始位置,
- 再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
效率分析
空间复杂度
O(n)
用于存储整个数列,O(1)
辅助,暂存操作数,用于交换。
时间复杂度
由于一共有n个位置要进行选择,每次选择时又要对剩下为放入合适位置的数进行便利,所以时间复杂度不分最好和最坏,依次比较(N-1)+(N-2)+ ... +1
次,即(N-1+1)*N/2 = (N^2)/2
, 为O(n^2)
。
算法实现
Python实现
def selection_sort(alist):
length = len(alist)
for index in range(length):
m = index
for i in range(index, length):
if alist[i] < alist[m]:
m = i
alist[m], alist[index] = alist[index], alist[m]
return alist
高效排序
归并排序
归并算法主要通过拆分和合并两个步骤来完成对数列的排序,将需要排序的数列拆分成尽量小的序列,然后重新合并,合并时按顺序排列,最终形成有序序列。
排序过程
迭代法
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针到达序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
递归法
- 假设序列共有n个元素:
- 将序列每相邻两个数字进行归并操作,形成n/2个序列,排序后每个序列包含两个元素
- 将上述序列再次归并,形成n/4个序列,每个序列包含四个元素
- 重复步骤2,直到所有元素排序完毕
效率分析
以递归法为例:
递归算法的时间由子问题的时间和合并子问题的时间两部分构成,最小子问题不需要合并,因此n=1
时即为ɵ(1)
。
进而得出以下方程:
T(n)
所要合并的的数列为原来数列中所有的数(n个
),所以时间为cn
。T(n/2)
为T(n)
划分出的子问题,它所处理的数列中数的个数是n/2
个,故T(n/2)=2T(n/4)+cn/2
,类推得到下图。
图中树的深度显然为(logn)+1
,每一层所有的问题所需要的合并时间的和都是cn
,因此总时间为cn(logn)+cn
,时间复杂度为O(nlogn)
。
时间复杂度:
根据上面的推导,归并排序的时间复杂度为O(nlogn)
。
空间复杂度
- 迭代法:
O(n)
存储整个数列,O(1)
辅助,用于交换。 - 递归法:
O(n)
存储整个数列,O(n)
辅助,用于子问题存储子问题的序列。
算法实现
C++迭代法实现
对于C++,可以使用模板使得函数适用于所有的类型的数据。
template<typename T>
void merge_sort(T arr[], int len) {
T* a = arr;
T* b = new T[len];
for (int seg = 1; seg < len; seg += seg) {
for (int start = 0; start < len; start += seg + seg) {
int low = start, mid = min(start + seg, len), high = min(start + seg + seg, len);
int k = low;
int start1 = low, end1 = mid;
int start2 = mid, end2 = high;
while (start1 < end1 && start2 < end2)
b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
while (start1 < end1)
b[k++] = a[start1++];
while (start2 < end2)
b[k++] = a[start2++];
}
T* temp = a;
a = b;
b = temp;
}
if (a != arr) {
for (int i = 0; i < len; i++)
b[i] = a[i];
b = a;
}
delete[] b;
}
Python递归法实现
def merge_sort(alist):
"""合并函数"""
if len(alist) <= 1:
return alist
middle = len(alist)//2
left = merge_sort(alist[:middle])
right = merge_sort(alist[middle:])
print left + right
return merge(left, right)
def merge(left, right):
"""主排序函数"""
l, r = 0, 0
result = []
while l<len(left) and r<len(right):
if left[l] < right[r]:
result.append(left[l])
l += 1
else:
result.append(right[r])
r += 1
return result+left[l:]+right[r:]
堆排序
堆是什么
堆可以看成是二叉树的一种,也可以看成是元素有优先权区别的队列。
- 任意节点小于(或大于)它的所有后裔(树中的子节点),最小元(或最大元)在的根上。
- 堆总是一棵完全树。(每一层必须从左往右排,并且排完一层之后才能排下一层)
像这样:👉
[图片上传失败...(image-779e73-1534946683303)]
(左边是一个小顶堆,右边是大顶堆。)
根据上面所说的,只有一个数的序列一定可以构成一个堆。
排序过程
以从小到大排为例:
- 最大堆调整:将末端的子节点进行调整,使得子节点小于父节点。
前面说过,只有一个数的序列一定可以构成一个堆,下面考虑三个数的情况。
如果要让这个子二叉树成为堆,只需要将树根与较大的子节点进行交换即可(7和15交换)。
如果将根中的数与子节点交换的过程看作是下沉的过程,那么它必须下沉到没有子节点比它小的位置,因为交换的过程中始终使较大的数移到上一层的位置,所以不会对其他数的排序造成影响。
- 建立最大堆:将堆所有数据重新排序。
- 堆排序:移除位在第一个数据(实际操作中将其放到数列的最后),对其他的数继续进行堆排序。
效率分析
时间复杂度
堆排序的时间主要由堆调整和建堆两部分构成。
- 堆调整
当前节点与两个子节点各比较一次之后与较大的进行一次交换,并对被交换的子节点进行递归操作。所以有T(n)=T(n-1)+3; T(1)=3
,树高h=logn
,所以T(h)=3h=3O(logn)
,堆调整的时间复杂度为O(logn)
。 - 建堆
树高h=logn
。最后一层的节点没有子节点,不用进行操作。对深度为于h-1
层的节点,比较2次,交换1次,这一层最多有2^(h-1)个节点,总共操作次数最多为3(1*2^(h-1))
;对深度为h-2
层的节点,总共有2^(h-2)
个,每个节点最多比较4次,交换2次,所以操作次数最多为3(2*2^(h-2))
。另a:s=3*[2^(h-1) + 2*2^(h-2)+ 3*2^(h-3) + … + h*2^0]
,b:2s=3*[2^(h) + 2*2^(h-1)+ 3*2^(h-2) + … + h*2^1]
,a-b得s=3*[2^h + 2^(h-1) + 2^(h-2) + … + 2 - h]=3*[2^(h+1)-2-h]
,又因为2^(h+1)=n+1
,所以总时间约为3n
,建堆的时间复杂度O(n)
。
综上:堆排序的时间等于第一次建堆加上后面n-1
次堆调整
,由于n的减小,后面的O(log2(n))中的n也会减小,所以用小于等于号T(n) <= O(n) + (n - 1)*O(log2(n))
,时间复杂度为O(nlogn)
空间复杂度
O(n)
存储整个数列,O(1)
辅助,用于交换。
算法实现
Python实现
def swap(array, a, b):
"""交换函数"""
temp = array[a]
array[a] = array[b]
array[b] = temp
def sift_down(array, last_index):
"""堆调整函数"""
index = 0
while True:
left_index = 2*index + 1
right_index = 2*index + 2
if left_index > last_index:
break
else:
if right_index > last_index:
next_index = left_index
else:
if array[left_index] >= array[right_index]:
next_index = left_index
else:
next_index = right_index
if array[next_index] <= array[index]:
break
temp = array[index]
array[index] = array[next_index]
array[next_index] = temp
index = next_index
print("next_index: ", next_index)
def heap_sort(array, length):
"""堆排序主函数"""
last_node = (length - 2) / 2
for i in range(last_node, 0, -1):
sift_down(array, length-1)
for i in range(length-1, 1, -1):
swap(array, 0, i)
sift_down(array, i-1)
swap(array, 0, 1)
快速排序
快速排序类似于上体育课排队的过程,总是以一个人为基准,其他人根据身高分列在他的两边。在快速排序中也有一个基准--枢轴(pivot),其他的数根据大小排在它的两边。之所以叫快速排序,是因为快速排序在最好情况和一般情况下的时间复杂度都是最低的。
排序过程
- 从数列中挑出一个元素,称为"基准"(pivot),
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
效率分析
时间复杂度
- 平均:
O(logn)
- 最好:Partition每次划分均匀,递归树的深度为
logn+1
,即仅需递归logn
次,第一次Partiation对整个数列扫描一遍,做n次比较。然后,获得的枢轴将数组一分为二,各自还需要T(n/2)
的时间。类推得到:
T(n)=2T(n/2)+n; T(1)=0
T(n)=2(2T(n/4)+n/2)+n=4T(n/4)+2n
T(n)=4(2T(n/8)+n/4)+2n=8T(n/8)+3n
所以T(n)≤nT(1)+(log2n)×n= O(nlogn)
-
最坏:每次划分只得到一个比上一次划分少一个记录的子序列,递归树除了叶节点之外的节点都只有一个子节点,每次都要与剩下的所有数进行比较。
空间复杂度
- 平均:
O(n)
存储整个数列,O(logn)
辅助 - 最好:
O(n)
存储整个数列,O(logn)
辅助 - 最坏:
O(n)
存储整个数列,O(n)
辅助
辅助空间来源于递归造成的栈空间的使用。
算法实现
Python实现
def Partition(r, low, high):
pivot = r[low]
while low < high:
while low < high and r[high] >= pivot:
high -= 1
if low < high:
r[low] = r[high]
low += 1
while low < high and r[low] <= pivot:
low += 1
if low < high:
r[high] = r[low]
high -= 1
r[low] = pivot
return low
def QuickSort(r, low, high):
if low < high:
pivotkey = Partition(r, low, high)
QuickSort(r, low, pivotkey-1)
QuickSort(r, pivotkey+1, high)
分配排序
计数排序
计数排序与之前的算法采用的是完全不同的一种视角,它注重的是元素应该存在的位置,而不再是两个元素之间的大小关系。
排序过程
- 找出待排序的数组中最大和最小的元素
- 统计数组中每个值为i的元素出现的次数,存入数组 C 的第 i 项
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
效率分析
时间复杂度
需要将数组遍历三遍,但是这三个循环不是嵌套执行的,所以时间复杂度没有影响。
平均:O(n+k)
空间复杂度
存放原序列和元素的计数信息:O(n+k)
计数排序的瓶颈十分明显,对于数据范围很大的数组,需要大量时间和内存。
算法实现
Python实现
下面是对一个序列中所有的字符进行排序
def countSort(arr):
output = [0 for i in range(256)]
count = [0 for i in range(256)]
for i in arr:
count[ord(i)] += 1
for i in range(256):
count[i] += count[i-1]
for i in range(len(arr)):
output[count[ord(arr[i])]-1] = arr[i]
count[ord(arr[i])] -= 1
return output
桶排序
排序过程
- 设置一个定量的序列当作空桶。
- 遍历序列,把元素放到对应的桶中去。
- 对每个不是空的桶子进行排序。
- 将桶中的元素放回到原来的序列中去。
效率分析
时间复杂度
与计数排序类似,遍历和桶中的排序是并列关系,不影响时间复杂度,平均O(n+k)
空间复杂度
桶的个数和每个桶中元素的个数,O(n*k)
算法实现
C++实现
C++中的vector是用来作桶的绝佳的材料。也可以用链表来实现。
void bucketSort(float arr[], int n)
{
vector<float> b[n];
for (int i=0; i<n; i++)
{
int bi = n*arr[i];
b[bi].push_back(arr[i]);
}
for (int i=0; i<n; i++)
sort(b[i].begin(), b[i].end());
int index = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < b[i].size(); j++)
arr[index++] = b[i][j];
}
基数排序
如果要将一副扑克牌恢复到原来有序的状态,为了将扑克牌恢复成有序的状态(就像刚买来时那样),我们通常先挑出相同花色的牌放在一起,然后再按照牌号的大小进行排序,也就是依次按照牌的不同属性进行排序。
而在基数排序中,通常将数的不同的位看作是不同的属性,也就是依次根据各个位上数字的大小进行排序。
排序过程
- 对数列中的数从最低位开始每次取一位进行比较,先比较个位,然后比较十位...
- 根据选中的位对元素进行计数排序
- 重复上述过程,直到取完所有位
效率分析
时间复杂度
假设最大的数一共有k位,每取一位进行比较都要讲所有的数遍历一遍,因此为O(kN)
空间复杂度
计数列表的空间和用做中间列表的存储的空间,O(k+N)
算法实现
Python实现
def countingSort(arr, exp1):
"""计数排序函数"""
n = len(arr)
output = [0] * (n) # 最终的数列,先用0占位
count = [0] * (10) # 每个数进行计数的列表,初始化为0
for i in range(0, n):
index = (arr[i]/exp1)
count[ (index)%10 ] += 1
for i in range(1,10):
count[i] += count[i-1]
i = n-1
while i>=0:
index = (arr[i]/exp1)
output[ count[ (index)%10 ] - 1] = arr[i]
count[ (index)%10 ] -= 1
i -= 1
# 将arr修改为按这一位排序过后的顺序
i = 0
for i in range(0,len(arr)):
arr[i] = output[i]
def radixSort(arr):
max1 = max(arr)
exp = 1
while max1/exp > 0: # 从个位开始每次取一位,进行计数排序
countingSort(arr,exp)
exp *= 10
其他
冒泡排序
冒泡排序与气泡上升的过程相似,气泡上升的过程中不断吸收空气而变大,只不过冒泡排序中的元素不会发生变化,而是较大的数与较小数交换了位置。冒泡排序是一种用时间换空间的算法。
排序过程
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
效率分析
时间复杂度
外层循环n-1
次,内层循环n-i
次。内层循环总的次数用等差数列求和公式是(1+(n-1))*(n-1)/2=n*(n-1)/2≈n^2/2
,外层循环赋值次数为常数设为a
,内层循环赋值次数也是常数设为b
,所以f(n)≈a * n + b * n^2/2
,时间复杂度是O(n^2)
空间复杂度
O(n)
存储整个数列,O(n)
辅助,用于交换。
算法实现
Python实现
def bubble_sort(alist):
length = len(alist)
for index in range(length-1):
for i in range(0, length-index-1):
if alist[i] > alist[i+1]:
alist[i+1], alist[i] = alist[i], alist[i+1]
return alist
优化: 添加标记,在排序完成时停止排序,可以使最好情况下的时间复杂度为O(n)
def bubble_sort_flag(alist):
length = len(alist)
for index in range(length):
flag = True
for i in range(0, length-index-1):
if alist[i] > alist[i+1]:
alist[i+1], alist[i] = alist[i], alist[i+1]
flag = False
if flag:
return alist
return alist
希尔排序
Donald Shell设计的算法,也称递减增量排序算法,利用了插入排序在对几乎已经排好序的数据操作时,效率高,可以达到线性排序的效率的特点,对算法进行了优化。
希尔排序通过步长来控制调整顺序时的比较的两个数之间的间隔,在排序开始阶段使用较大的步长可以使一个元素可以一次性地朝最终位置前进一大步,然后再换用较小的步长,进行更加精确的调整。
算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。
排序过程
与插入排序类似,只不过不再是按照原序列的顺行依次进行判断了,而是在无序序列中每次间隔步长个元素取元素,对其进行插入。
步长选择
希尔增量
步长选择为n\2
并且对步长取半直到步长达到1
。
Sedgewick增量
1, 5, 19, 41, 109,...
,根据
斐波那契增量
斐波那契数列除去0和1将剩余的数以黄金分割比的两倍的幂进行运算得到的数列:1, 9, 34, 182, 836, 4025, 19001, 90358, 428481, 2034035, 9651787, 45806244, 217378076, 1031612713,…
效率分析
时间复杂度
- 平均: 根据选择的步长的不同而不同,通常为
O(n^2)
,但比起他时间复杂度为O(n^2)
的算法更快。 - 最好:序列已经有序,遍历一遍即可,为
O(n)
。 - 最坏:
O(n^2)
空间复杂度
O(n)用于存储整个数列,O(1)辅助,暂存操作数,用于插入。
算法实现
Python实现
def shell_sort(alist):
length = len(alist)
gap = length / 2
while gap > 0:
for i in range(gap, length):
temp = alist[i]
j = i
# 插入排序
while j >= gap and alist[j-gap] > temp:
alist[j] = alist[j-gap]
j -= gap
alist[j] = temp
gap = gap / 2
return alist
梳排序(Comb Sort)
梳排序的基本思想和 希尔排序 一样,都是通过在排序开始阶段用较大的步长进行排序,使得一个元素可以一次性地朝最终位置前进一大步。相对于 希尔排序 对 插入排序 进行改良,梳排序 则是 冒泡排序 的延伸。
排序过程
梳排序基于冒泡排序,但是没次与固定距离处的数的比较和交换,这个固定距离是待排数组长度除以1.3
(一个魔数)得到近似值,下次则以上次得到的近似值再除以1.3
,直到距离小至3
时,以1
递减。
效率分析
时间复杂度
- 平均:
ꭥ((n^2)/(2^p))
,p为数据的增量。 - 最好:
Ɵ(nlogn)
- 最坏:
O(n^2)
空间复杂度
O(n)用于存储整个数列,O(1)辅助,用于交换。
算法实现
Python实现
def comb_sort(alist):
shrink = 1.3
gap = len(alist)
while True:
gap = int(gap / shrink)
i = 0
if gap < 1:
break
else:
while i + gap < length:
if alist[i] > alist[i+gap]:
alist[i], alist[i+gap] = alist[i+gap], alist[i]
i += 1
return alist