原文连接:http://kasheemlew.github.io/2...node
想象一下插队的过程...git
一我的经过插队的方式排到前面,而将本来在他前面的人挤到了后面的位置。
对数排序时也是这样,为了从小到大排序,须要将一个数放到前面,而将那些比它大的数挤到了后面,从而实现了排序的目的。github
当一个数列A开始进行排序时就已经被划分红了两个部分--有序的和无序的,因为只有一个数的数列能够被看做已经有序,因此将数列A中的第一个元素看做有序的部分(图中6),后面的其余数看做无序的部分(图中5,3,1,8,7,2,4)算法
从前到后扫描有序的部分,将无序部分中的第一个元素插入到有序的部分中合适的位置(3插到5,6的前面)shell
而后将有序序列中该数后面的数依次后移一位(5,6后移一位),造成新的有序部分(3, 5, 6)数组
重复直到遍历完整个数列。app
O(n)用于存储整个数列,O(1)辅助,暂存操做数,用于插入。函数
最好:已经有序的状况下,只须要遍历数列一次,为O(n)
。优化
最坏:反序状况下比较次数依次是1 + 2 + 3 + ... + (N - 1)
,即(1/2)n(n-1)
次。O(n^2)
。ui
平均:O(n^2)
根据上述的排序过程,易用数组进行实现,这里再也不赘述。但使用链表实现中每次后移的过程会大大下降排序的效率,如下两种实现能够
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中的切片,每次插入一个数以后没必要再使用后移的方式调整有序序列的位置,而是直接拼接切片并返回。
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)
。
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++,可使用模板使得函数适用于全部的类型的数据。
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; }
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:]
堆能够当作是二叉树的一种,也能够当作是元素有优先权区别的队列。
任意节点小于(或大于)它的全部后裔(树中的子节点),最小元(或最大元)在的根上。
堆老是一棵彻底树。(每一层必须从左往右排,而且排完一层以后才能排下一层)
像这样:?
(左边是一个小顶堆,右边是大顶堆。)
根据上面所说的,只有一个数的序列必定能够构成一个堆。
以从小到大排为例:
最大堆调整:将末端的子节点进行调整,使得子节点小于父节点。
前面说过,只有一个数的序列必定能够构成一个堆,下面考虑三个数的状况。
若是要让这个子二叉树成为堆,只须要将树根与较大的子节点进行交换便可(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)
辅助,用于交换。
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)
辅助
辅助空间来源于递归形成的栈空间的使用。
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)
计数排序的瓶颈十分明显,对于数据范围很大的数组,须要大量时间和内存。
下面是对一个序列中全部的字符进行排序
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++中的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)
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)
辅助,用于交换。
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
。
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)辅助,暂存操做数,用于插入。
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
梳排序的基本思想和 希尔排序 同样,都是经过在排序开始阶段用较大的步长进行排序,使得一个元素能够一次性地朝最终位置前进一大步。相对于 希尔排序 对 插入排序 进行改良,梳排序 则是 冒泡排序 的延伸。
梳排序基于冒泡排序,可是没次与固定距离处的数的比较和交换,这个固定距离是待排数组长度除以1.3
(一个魔数))获得近似值,下次则以上次获得的近似值再除以1.3
,直到距离小至3
时,以1
递减。
平均:ꭥ((n^2)/(2^p))
,p为数据的增量。
最好:Ɵ(nlogn)
最坏:O(n^2)
O(n)用于存储整个数列,O(1)辅助,用于交换。
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