参考
Python:
http://wuchong.me/blog/2014/02/09/algorithm-sort-summary/
Java:
https://www.cnblogs.com/10158wsj/p/6782124.html?utm_source=tuicool&utm_medium=referral
稳定性解释:
https://baike.baidu.com/item/%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E7%A8%B3%E5%AE%9A%E6%80%A7/9763250?fr=aladdin
性能分析与适应场景:
http://blog.csdn.net/p10010/article/details/49557763
动画:
http://blog.csdn.net/tobeandnottobe/article/details/7192953
http://www.webhek.com/post/comparison-sort.html
性能
只需记住一句话(快些选一堆美女一起玩儿)是不稳定的,其他都是稳定的。
初始数据越无序,快速排序越好。
实现
冒泡排序
Python:
def bubble_sort(array):
n = len(array)
for i in range(n): # i从0到n
for j in range(1, n-i): # 1开始,即j-1=0开始
if array[j-1] > array[j]:
array[j-1], array[j] = array[j], array[j-1]
return array
#优化1:某一趟遍历如果没有数据交换,则说明已经排好序了,因此不用再进行迭代了。
#用一个标记记录这个状态即可。
def bubble_sort2(ary):
n = len(ary)
for i in range(n):
flag = 1 #标记
for j in range(1,n-i):
if ary[j-1] > ary[j] :
ary[j-1],ary[j] = ary[j],ary[j-1]
flag = 0
if flag : #全排好序了,直接跳出
break
return ary
#优化2:记录某次遍历时最后发生数据交换的位置,这个位置之后的数据显然已经有序了。
# 因此通过记录最后发生数据交换的位置就可以确定下次循环的范围了。
def bubble_sort3(ary):
n = len(ary)
k = n #k为循环的范围,初始值n
for i in range(n):
flag = 1
for j in range(1,k): #只遍历到最后交换的位置即可
if ary[j-1] > ary[j] :
ary[j-1],ary[j] = ary[j],ary[j-1]
k = j #记录最后交换的位置
flag = 0
if flag :
break
return ary
Java:
public void bubble_sort(int [] a) {
int n = a.length;
for(int i=0;i<n;i++) {
for(int j=1;j<n-i;j++) {
if (a[j-1] > a[j]) {
int temp = a[j-1];
a[j-1] = a[j];
a[j] = temp;
}
}
}
}
//两种优化不写了
选择排序
Python:
def selection_sort(array):
n = len(array)
for i in range(n):
minIndex = i
for j in range(i+1, n):
if array[j] < array[minIndex]:
minIndex = j
array[i], array[minIndex] = array[minIndex], array[i]
# 或者使用minNum存储数值,避免每次都读array[minIndex],但如果每次都存储新的minNum,也会有损耗。
def selection_sort(array):
n = len(array)
for i in range(n):
minNum = array[i]
minIndex = i
for j in range(i+1, n):
if array[j] < minNum:
minIndex = j
minNum = array[j]
array[i], array[minIndex] = array[minIndex], array[i]
Java:
public void selection_sort(int [] a) {
int n = a.length;
for(int i=0;i<n;i++) {
int minIndex = i;
for(int j=i;j<n;j++) {
if (a[j] < a[minIndex]) {
minIndex = j;
}
}
int temp = a[i];
a[i] = a[minIndex];
a[minIndex] = temp;
}
}
插入排序
Python:
def insert_sort(array):
n = len(array)
for i in range(1,n): # 从第二个数开始
if array[i-1] > array[i]: # 前面比后面大,需要调整位置
num = array[i]
index = i
for j in range(i-1, -1, -1):
if array[j] > num:
array[j+1] = array[j]
index = j
else: # 找到位置,插入
array[index] = num
break
Java:
public void insert_sort(int [] a) {
int n = a.length;
for(int i=1;i<n;i++) {
if (a[i-1] > a[i]) {
int num = a[i];
int index = i;
for(int j=i-1;j>-1;j--) {
if (a[j] > num) {
a[j+1] = a[j];
index = j;
}
else {
a[index] = num;
break;
}
}
}
}
}
希尔排序(递减增量排序算法,实质是分组插入排序)
Python:
def shell_sort(ary):
n = len(ary)
gap = round(n/2) # 初始步长 , 用round取整(注意0.5向下取)
while gap > 0 :
for i in range(gap,n): # 每一列进行插入排序 , 从gap 到 n-1
temp = ary[i]
j = i
print(temp, j)
while ( j >= gap and ary[j-gap] > temp ): # 插入排序
ary[j] = ary[j-gap]
j = j - gap
ary[j] = temp
gap = round(gap/2) # 重新设置步长
return ary
Java:
public void shell_sort(int [] a) {
int n = a.length;
int gap = n / 2;
while (gap > 0) {
for (int i=gap;i<n;i++) {
int temp = a[i];
int j = i;
while (j>=gap && a[j-gap] > temp) {
a[j] = a[j-gap];
j = j - gap;
}
a[j] = temp;
}
gap = gap / 2;
}
}
归并排序(递归合并)
Python:
def merge_sort(array): # 递归
if len(array) <= 1: return array # python每次都是新的数组,可以用数组长度小于等于1来判断
num = len(array) // 2 # py27 3/2和3//2相同,python3 3//2才是地板除
left = merge_sort(array[:num])
right = merge_sort(array[num:])
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 = l + 1
else:
result.append(right[r])
r += 1
# 一边没有之后,加上所有的
result += left[l:]
result += right[r:]
return result
Java:
//注意:新建的temp长度和原数组是一样的,所以额外空间是O(n),temp数组一开始并未赋值,在合并时慢慢给其填充数值,所以说一共只有一个temp数组
public void mergeSort(int[] arr) {
mergeSort(arr, new int[arr.length], 0, arr.length - 1);
}
private static void mergeSort(int[] arr, int[] temp, int left, int right) {
if (left < right) { // Java则通过左右指针来判断
int center = (left + right) / 2;
mergeSort(arr, temp, left, center); // 左边
mergeSort(arr, temp, center + 1, right); // 右边
merge(arr, temp, left, center + 1, right); // 合并两个有序
}
}
private static void merge(int[] arr, int[] temp, int leftPos, int rightPos, int rightEnd) {
int leftEnd = rightPos - 1; // 左边结束下标
int tempPos = leftPos; // 从左边开始算
int numEle = rightEnd - leftPos + 1; // 元素个数
while (leftPos <= leftEnd && rightPos <= rightEnd) {
if (arr[leftPos] <= arr[rightPos])
temp[tempPos++] = arr[leftPos++];
else
temp[tempPos++] = arr[rightPos++];
}
while (leftPos <= leftEnd) { // 左边如果有剩余
temp[tempPos++] = arr[leftPos++];
}
while (rightPos <= rightEnd) { // 右边如果有剩余
temp[tempPos++] = arr[rightPos++];
}
// 将temp复制到arr,覆盖原来这里的位置
for (int i = 0; i < numEle; i++) {
arr[rightEnd] = temp[rightEnd];
rightEnd--;
}
}
快速排序
我暂时认为有两种交换方法,中间排序过程不同,但结果都对
方法一:youtube视频:https://www.youtube.com/watch?v=gl_XQHTJ5hY (下图代码,并且是两两交换,最后将key与left交换)
方法二:http://blog.csdn.net/morewindows/article/details/6684558 (key一开始就被挖坑填写了别的数,经过几次牛客网选择题,我认为第二种是做选择题时需要掌握的的)
下图:分别对应第一种和第二种
Python:
def quick_sort(ary):
return _quick_sort(ary, 0, len(ary)-1)
def _quick_sort(ary, left, right):
if left >= right: return ary
key = ary[left] # 每次都选最左边为key
lp = left
rp = right
while (lp < rp):
while ary[rp] >= key and lp < rp:
rp -= 1
while ary[lp] <= key and lp < rp:
lp += 1
ary[lp], ary[rp] = ary[rp], ary[lp]
ary[left], ary[lp] = ary[lp], ary[left] # 这里不能用key,是交换数组内数字
_quick_sort(ary, left, lp-1)
_quick_sort(ary, lp+1, right) # 这里lp, rp永远是相等的。所以都可以。
return ary
Java:
public void quick_sort(int[] ary) {
_quick_sort(ary, 0, ary.length-1);
}
public void _quick_sort(int[] ary, int left, int right) {
if (left < right) {
int key = ary[left];
int lp = left;
int rp = right;
while (lp < rp) {
while (ary[rp] >= key && lp < rp ) {
rp--;
}
while (ary[lp] <= key && lp < rp ) {
lp++;
}
int temp = ary[lp];
ary[lp] = ary[rp];
ary[rp] = temp;
}
int temp = ary[lp];
ary[lp] = ary[left];
ary[left] = temp;
_quick_sort(ary, left, lp-1);
_quick_sort(ary, rp+1, right);
}
}
堆排序
http://blog.csdn.net/minxihou/article/details/51850001
https://www.2cto.com/kf/201609/549335.html
例题:相当帮助理解
https://www.nowcoder.com/test/question/done?tid=14276624&qid=56294#summary
父节点i的左子节点在位置(2*i+1);
父节点i的右子节点在位置(2*i+2);
子节点i的父节点在位置floor((i-1)/2);
堆排序构建堆的时间复杂度是N,而重调堆的时间复杂度是logN
堆可以分为大根堆和小根堆,这里用最大堆的情况来定义操作:
(1)最大堆调整(MAX_Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点。这是核心步骤,在建堆和堆排序都会用到。比较i的根节点和与其所对应i的孩子节点的值。当i根节点的值比左孩子节点的值要小的时候,就把i根节点和左孩子节点所对应的值交换,当i根节点的值比右孩子的节点所对应的值要小的时候,就把i根节点和右孩子节点所对应的值交换。然后再调用堆调整这个过程,可见这是一个递归的过程。
(2)建立最大堆(Build_Max_Heap):将堆所有数据重新排序。建堆的过程其实就是不断做最大堆调整的过程,从len/2出开始调整,一直比到第一个节点。
(3)堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算。堆排序是利用建堆和堆调整来进行的。首先先建堆,然后将堆的根节点选出与最后一个节点进行交换,然后将前面len-1个节点继续做堆调整的过程。直到将所有的节点取出,对于n个数我们只需要做n-1次操作。堆是用顺序表存储的的代码可以先看:http://blog.51cto.com/ahalei/1427156 就能理解代码中的操作
注意:从小到大排序的时候不建立最小堆而建立最大堆。最大堆建立好后,最大的元素在h[ 1]。因为我们的需求是从小到大排序,希望最大的放在最后。因此我们将h[ 1]和h[ n]交换,此时h[ n]就是数组中的最大的元素。请注意,交换后还需将h[ 1]向下调整以保持堆的特性。OK现在最大的元素已经归位,需要将堆的大小减1即n–,然后再将h[ 1]和h[ n]交换,并将h[ 1]向下调整。如此反复,直到堆的大小变成1为止。此时数组h中的数就已经是排序好的了。代码如下:
Python:
def MAX_Heapify(heap, HeapSize, root): # 在堆中做结构调整使得父节点的值大于子节点
left = 2 * root + 1
right = left + 1
larger = root
if left < HeapSize and heap[larger] < heap[left]:
larger = left
if right < HeapSize and heap[larger] < heap[right]: # 确定到底和左还是右节点换,先判断做节点
larger = right
if larger != root: # 如果做了堆调整:则larger的值等于左节点或者右节点的,这个时候做对调值操作
heap[larger],heap[root] = heap[root],heap[larger]
MAX_Heapify(heap, HeapSize, larger) # 从顶端递归往下调整,用larger是因为larger是数组索引,并且已经在比较时候更新过,而root没有更新过!
def Build_MAX_Heap(heap): # 构造一个堆,将堆中所有数据重新排序
HeapSize = len(heap)
for i in range((HeapSize -2)//2,-1,-1): # 从后往前出数 '//' got integer
MAX_Heapify(heap,HeapSize,i) # 这里堆的大小是固定,root是i逐步减小
def HeapSort(heap): # 将根节点取出与最后一位做对调,对前面len-1个节点继续进行对调整过程。
Build_MAX_Heap(heap)
for i in range(len(heap)-1,-1,-1):
heap[0],heap[i] = heap[i],heap[0]
MAX_Heapify(heap, i, 0) # 这里堆的大小是逐步缩小,root永远是0
return heap
Java:
有空补
非基于比较的排序算法
基于比较的排序算法是不能突破O(NlogN)的。简单证明如下:
N个数有N!个可能的排列情况,也就是说基于比较的排序算法的判定树有N!个叶子结点,比较次数至少为log(N!)=O(NlogN)(斯特林公式)。
计数排序
计数排序在输入n个0到k之间的整数时(可以从a到b,不用非要从0开始,代码可以实现),时间复杂度最好情况下为O(n+k),最坏情况下为O(n+k),平均情况为O(n+k),空间复杂度为O(n+k)
算法的步骤如下:
1.找出待排序的数组中最大和最小的元素
2.统计数组中每个值为i的元素出现的次数,存入数组C的第i项
3.对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
4.反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
当k不是很大时,这是一个很有效的线性排序算法。更重要的是,它是一种稳定排序算法,即排序后的相同值的元素原有的相对位置不会发生改变(表现在Order上),这是计数排序很重要的一个性质,就是根据这个性质,我们才能把它应用到基数排序。
# -*- coding:utf-8 -*-
def count_sort(ary):
max=min=0 # min和max应该用sys.maxint
for i in ary:
if i < min:
min = i
if i > max:
max = i
count = [0] * (max - min +1)
for index in ary:
count[index-min]+=1
index=0
for a in range(max-min+1):
for c in range(count[a]): # 重点:有多少个就for循环多少次
ary[index]=a+min
index+=1
return ary
桶排序
假如待排序列K= {49、 38 、 35、 97 、 76、 73 、 27、 49 }。这些数据全部在1—100之间。因此我们定制10个桶,然后确定映射函数f(k)=k/10。则第一个关键字49将定位到第4个桶中(49/10=4)。依次将所有关键字全部堆入桶中,并在每个非空的桶中进行快速排序。
因此,我们需要尽量做到下面两点:
(1) 映射函数f(k)能够将N个数据平均的分配到M个桶中,这样每个桶就有[N/M]个数据量。
(2) 尽量的增大桶的数量。极限情况下每个桶只能得到一个数据,这样就完全避开了桶内数据的“比较”排序操作。 当然,做到这一点很不容易,数据量巨大的情况下,f(k)函数会使得桶集合的数量巨大,空间浪费严重。这就是一个时间代价和空间代价的权衡问题了。
对于N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:
O(N)+O(M*(N/M)log(N/M))=O(N+N(logN-logM))=O(N+N*logN-N*logM)
当N=M时,即极限情况下每个桶只有一个数据时。桶排序的最好效率能够达到O(N)。
桶排序是稳定的。
基数排序
基数排序的思想就是将待排数据中的每组关键字依次进行桶分配。比如下面的待排序列:
278、109、063、930、589、184、505、269、008、083
我们将每个数值的个位,十位,百位分成三个关键字: 278 -> k1(个位)=8 ,k2(十位)=7 ,k3=(百位)=2。
然后从最低位个位开始(从最次关键字开始),对所有数据的k1关键字进行桶分配(因为,每个数字都是 0-9的,因此桶大小为10),再依次输出桶中的数据得到下面的序列。
930、063、083、184、505、278、008、109、589、269
再对上面的序列接着进行针对k2的桶分配,输出序列为:
505、008、109、930、063、269、278、083、184、589
最后针对k3的桶分配,输出序列为:
008、063、083、109、184、269、278、505、589、930
很明显,基数排序的性能比桶排序要略差。每一次关键字的桶分配都需要O(N)的时间复杂度,而且分配之后得到新的关键字序列又需要O(N)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2N) ,当然d要远远小于N,因此基本上还是线性级别的。基数排序的空间复杂度为O(N+M),其中M为桶的数量。一般来说N>>M,因此额外空间需要大概N个左右。
但是,对比桶排序,基数排序每次需要的桶的数量并不多。而且基数排序几乎不需要任何“比较”操作,而桶排序在桶相对较少的情况下,桶内多个数据必须进行基于比较操作的排序。因此,在实际应用中,基数排序的应用范围更加广泛。