本文所有排序算法均使用python
第一部分:O(N^2)的排序算法
学过的人应该都知道,排序算法最好的应该是O(N log N)之类的排序算法,但是O(N^2)的算法是这些算法的基础,主要用途在于:
- 因为简单,易于实现,所以它是简单情境的首选;
- 复杂的排序算法思想是从简单的排序算法思想中衍生出来的,比如说希尔排序;
- 而作为子过程,又可以用来改进更加复杂的算法
1.冒泡排序(Bubble Sort)
1.1 冒泡排序原理
冒泡排序是最简单的排序算法了,它的想法也很简单,如果是从小到达排序的话,就是每次比较两个元素,看他们的顺序有没有错,如果错了就换过来。
1.2 冒泡排序算法步骤
A:比较两个元素,如果第一个比第二个大,则两个元素交换位置
B:对每一对相邻的元素做同样的工作,从第一对到最后一对。这一次执行完,最后一个元素是最大的元素。
C:除了最后一个元素之外,对其余元素做同样的操作
D:重复以上A,B,C三步,直到每个元素都不需要交换位置为止
1.3 算法实现
def bubbleSort(arr):
for i in range(1,len(arr)):
for j in range(0,len(arr) - i):
if arr[j] > arr[j+1]:
arr[j] , arr[j+1] = arr[j+1] , arr[j]
return arr
1.4 算法效率
冒泡排序在所有数据都是正序的时候,最快(都排好序了,目的已达到)。当所有数据都排反序的时候最慢(用for循环反序输出就OK了)。
2.选择排序
2.1 选择排序原理
第一遍循环找到最小的元素放到第一位,第二次循环找到一个第二小元素,放到第二位,以此完成排序。在每次循环中,先把第一个元素标记为最小元素,然后将它与其后面的元素比较,发现比它小的,就将其标记为本次循环的最小元素。
无论什么数据进入,时间复杂度都是
2.2 算法步骤
A:先从未排序序列中找到最小(大)元素放到起始位置
B:再从剩余未排序序列中继续寻找最小(大)元素放到已排序末位
C:重复以上两步,直到排序完成
2.3 算法实现
def selectSort(arr):
#关键点:找到[i,n]中的最小值
for i in range(len(arr)-1):
for j in range( i+1 , len(arr)):
if arr[j] < arr[i]:
arr[i] ,arr[j] = arr[j],arr[i]
return arr
3.插入排序
3.1 插入排序原理
只要打过扑克牌的人应该都能非常好的理解插入排序。它的原理是通过构建有序序列,对与未排序序列,从后向前扫描,逐个比较之后,插入到相应的位置上。
3.2 算法步骤
A:将第一待排序序列的第一个元素看成有序序列,将第二个元素到最后一个元素当成未排序序列
B:从头到尾扫描未排序序列,将其插到已排序序列的的适当位置(如果两个元素相等,则插入到已排序序列元素的后面)。
3.3 算法实现
def insertionSort(arr):
for i in range(len(arr)):
preindex = i-1
current = arr[i]
while preindex >=0 and arr[preindex]>current:
arr[preindex +1] = arr[preindex]
preindex -= 1
arr[preindex + 1] = current
return arr
难点:
- 往前遍历
优点之一就是第二层循环在找到合适的插入位置之后可以提前终止,同时直接使用赋值操作,不像c++中使用swap来交换位置。如此得出的效果要更好,特别是当数组为近乎有效的数组时,插入排序的效率与NlogN级别的排序算法相差无几。而且当数组是完全有序的数组时,插入排序算法的性能可以达到O(n),因为第二层循环可以直接终止,而只进行第一层循环,因此插入排序的思想在接下来的算法能帮助我们改进算法。
4.希尔排序
4.1算法思想
希尔排序也称递减增量排序算法,是插入排序的改进版本。希尔排序是根据插入排序的以下两点性质而提出的改进的:
插入排序对已经排好序的序列效率高,可以达到线性排序的效率;
但插入排序是低效的,因为每次只能将数据移动一位;
希尔排序的思想是:先将待排序序列分割成若干个子序列,然后将每个子序列用插入排序进行排序,待子序列中的记录“基本有序”时,再对所有子序列进行一次直接插入排序。
4.2 算法步骤
4.3 算法实现
import math
def shellSort(arr):
gap = 1
while(gap < len(arr)/3):
gap = gap*3 + 1
while gap>0:
for i in range(gap,len(arr)):
temp = arr[i]
j = i-gap
while j>=0 and arr[j] > temp:
arr[j+gap] = arr[j]
j -= gap
arr[j + gap] = temp
gap = math.floor(gap/3)
return arr
第二部分:NlogN算法
5.归并排序(merge sort)
5.1 算法思想
归并算法采用的是分而治之(divide and conquer),他建立在归并操作上,归并操作是指将两个已经排序的序列合成一个序列,他是归并算法的基石。
因此,在把两个已经排序的数组放到一起时,需要额外的申请空间。在实际的应用中,我们优先考虑的是时间复杂度。那具体应该怎么做呢?
我们的做法就是确定三个索引在数组内进行追踪,蓝色箭头 表示在归并的过程中我们要跟踪的位置,两个红色的箭头表示两个待归并的数组当前的头元素。
归并排序的特点之一就是这三个箭头。
5.2 算法步骤
A:申请空间,使其大小为两个已经排序的序列之和,用来存放合并后的序列
B:设定两个指针,最初位置为两个已经排序序列的起始位置
C:选择两个指针代表的元素,并比较两个元素,选择相对较小的元素放到合并空间,并将指针移动下一个位置
D:重复步骤C,直到某一指针指向序列尾
E:将另一序列的所有元素直接复制到合并序列尾
5.3 算法实现
def mergeSort(arr):
if(len(arr)<2):
return arr
middle = int(len(arr)/2)
left,right = arr[:middle],arr[middle:]
return merge(mergeSort(left),mergeSort(right))
def merge(left,right):
result = []
while left and right:
if left[0]<=right[0]:
#为了不用具体的索引,我们直接把它pop出来
result.append(left.pop(0))
else:
result.append(right.pop(0))
while left:
result.append(left.pop(0))
while right:
result.append(right.pop(0))
return result
lst = [111,333,555,666,77,777,1,11,4,6,7,9]
mergeSort(lst)
结果:[1, 77, 111, 333, 555, 666, 777, 4, 11, 6, 7, 9]
6.快速排序
6.1 快速排序原理
快速排序的原理就是对于一个数组,从前找一个比首元素大的元素,从后找一个比最后一个元素小的元素,然后将两个元素进行交换。
如图所示,快速排序选择第一个元素放到第四的位置上,然后将所有元素分为两个个部分,第一部分小于4,第二部分大于4.然后再对这两部分再重新选择像4这样的标定点,使用快速排序的思想排序,知道整个数组全部有序为止。这个将数组拆分的操作称为partition.
6.2 算法步骤
- 从数组中选出一个元素作为标定点v,定义对应的三个索引,v为l,小于v的最后一个元素为j,当前考虑的元素为i,如图所示
- partition操作:以标定点将数组拆分成小于v和大于v的,
- 对于当前的元素e,索引为i,将e与v比较后,
- 如果它大于v,直接将索引移到下一个元素即可,i+1
- 如果它小于v,将i和j+1对应的元素互换位置,j+1
- 递归得把小于基准值的子数组和大于基准值的子数组排序
6.3 python实现
第一版实现与优化:
def quickSort(arr,left = None,right = None):
left = 0 if not isinstance(left,(int,float)) else left
right = len(arr) - 1 if not isinstance(right,(int,float)) else right
if left < right:
if(right - left < 16): #优化1,当数组列表足够小是采用插入排序
insertionSortForQS(arr,left,right)
else:
p = partition(arr,left,right)
quickSort(arr,left,p -1 )
quickSort(arr, p + 1,right)
return arr
def partition(arr,left,right):
v = arr[left]
j = left + 1
i = j
while i <= right:
if arr[i] < v:
swap(arr,j , i)
j += 1
i += 1
swap(arr,j - 1,left)
return j - 1
def swap(arr,i,j):
arr[i], arr[j] = arr[j], arr[i]
def insertionSortForQS(arr,first,last):
#专门为辅助快速排序设计的插入排序
for i in range(first+1,last+1):
curr = arr[i]
index = i
while index > first and arr[index-1]>curr:
arr[index]=arr[index-1]
index=index-1
arr[index]=curr
return arr
第二版优化:解决近乎有序数组使得算法达到O(n^2)的问题
快速排序的数组是完全有序的数组,因为我们每次选择的都是最左边的元素作为标定点,这样来切分会达到O(n^2)的时间复杂度,为了解决这个问题,我们可以随机的取标定点。
此时快速排序的时间复杂度的期望值为nlogn.
def partition(arr,left,right):
pivot = randint(left,right) #取随机标定点
swap(arr,left,pivot)
v = arr[left]
j = left + 1
i = j
while i <= right:
if arr[i] < v:
swap(arr,j , i)
j += 1
i += 1
swap(arr,j - 1,left)
return j - 1
第三版优化:针对有大量重复元素的列表:实现双路快速排序
由前一版的代码可以看到我们已经默认把元素划分成了<v和>=v
这种情况,但如果列表中的元素有大量重复值时,快速排序算法就会出现如下的情况:
两个部分依然有很大的可能出现极度不平衡的情况,因此,我们可以换一个写partition的思路,使用双路快速排序:
对于<v
的部分从前往后遍历,如果小于v,索引直接+1,对于>v
的部分,从后往前遍历,如果大于v,索引-1,如果不满足两种情况,把i,j两个位置的元素交换,这个步骤相当于把=v的元素分配到两边,使得生成的树不这么倾斜。
def partition2Ways(arr,left,right):
pivot = randint(left,right)
swap(arr,left,pivot)
v = arr[left]
i = left + 1
j = right
done = False
while not done:
while arr[i] < v and i <= right:
i += 1
while arr[j] > v and j >= left:
j -= 1
if i > j:
done = True
else:
swap(arr,i,j)
i += 1
j -= 1
swap(arr,left,j)
#当循环结束的时候,i位于第一个大于v的位置,j位置最后一个小于v的元素,故把第一个标定点元素和最后一个小于v的元素(j对应的位置)互换位置。最后返回j
return j
第四版优化:针对有大量重复元素的列表:实现三路快速排序
事实上,如果存在大量的重复元素可以直接使用三路快速排序更好,图示为程序运行的中间状态:
多定义两个索引,lt指向小于v的最后一个元素,gt指向大于v的第一个元素。
- 如果当前元素arr[i]=v,直接i+= 1
- 如果arr[i] < v,把arr[i]和arr[lt+1]互换,同时lt+=1,i += 1
- 如果arr[i]> v,把arr[gt-1]和arr[i]互换,同时i += 1
- 最后lt停止在等于v的第一个元素,gt停止在大于v的第一个元素
#实现三路快速排序
def quickSort3Ways(arr,left = None,right = None):
left = 0 if not isinstance(left,(int,float)) else left
right = len(arr) - 1 if not isinstance(right,(int,float)) else right
if left < right:
if(right - left <16): #优化1:当列表足够小时,采用插入排序
insertionSortForQS(arr,left,right)
return
pivot = randint(left, right)
swap(arr,left,pivot)
v = arr[left]
#为了保证初始的区间时空的,做如下设定
lt = left #arr[lt+1,...lt] < v
gt = right + 1 #arr[gt,...,r] > v
i = left + 1 #arr[lt+1,...r) == v
while i < gt:
if arr[i] < v:
swap(arr,lt + 1, i)
i += 1
lt += 1
elif arr[i] > v:
swap(arr,gt-1, i)
gt -= 1
else:
i += 1
swap(arr,left,lt)
quickSort3Ways(arr,left,lt - 1) #swap之后没有对lt进行-=1操作,所以这里使用lt-1
quickSort3Ways(arr, gt, right)
return arr