插入排序
对于一个n位数组A[n],从第二个元素开始(令key=A[n]),不断与前一个元素比较,如果前一个元素大于(小于)key,则前一个元素后移一位,key继续与更前一位进行比较。直到第一个元素也不满足或者有一个元素小于(大于)key,则停止比较,将key插入当前空出来的位置;然后将后一个元素赋予key,继续以上操作
- 输入:数组A[n]
- 输出:该数组的一个排列(按升或降的顺序)
伪代码如下:
for j = 2 to A.length
//所有的数组均从1开始
key = A[j];
i = j - 1;
while i > 0 && key< A[i]
A[i + 1] = A[i];
i--;
A[i + 1] = key;
时间复杂度:最坏O(n²),最好O(n),平均O(n²)。
貌似有个shell排序,时间复杂度平均能有O(n^1.3)。缺点是不会写。。。
归并排序
分治思想:
- 分解:将待排序的n个元素序列分成各具有n/2个元素的子序列
- 解决:使用归并排序递归的排序两个子序列
- 合并:合并两个已排序的子序列
很明显,这需要不断地递归地对序列进行地划分,直到出现元素个数为1的序列,此时该子序列已排序,然后开始向上返回。
将两个已经排序的子序列进行合并,思想就是从两个子序列的第一位开始,逐个比较两个子序列中的元素大小,大的(小的)放到总数组中,同时该子序列标志加一。、
这里为了简化过程,在子序列的尾设置一个哨兵,作为子序列的最大值(序列中永远不会有比它小的),这样可以仅仅通过比较大小将两个子序列全部放进主序列中,而不用判断越界问题。哨兵选用∞或者-∞。
伪代码如下:
//A为总序列,p到r为第一个已排序的子序列,r到q为第二个已排序的子序列
Merge(A, p, q, r)
{
n1 = q - p + 1; //第一个子序列的元素个数
n2 = r - q; //第二个子序列的元素个数
L = new Arrays(n1);
R = new Arrays(n2); //分别建立盛放两个子序列的L、R序列
for i = 1 to n1
L[i] = A[p + i - 1];
for j = 1 to n2
L[j] = A[q + j];
//以上两个循环分别建立L和R序列
L[n1 + 1] = ∞;
R[n2 + 1] = ∞;
//设置哨兵
i = 1;
j = 1;
//主序列从头到尾,子序列比较大小。。。
for k = p to r
if L[i] <= R[j]
A[k] = L[i++];
else if L[i] > R[j]
A[k] = R[j++];
}
//主函数:
MergeSort(A,p,r) //A为待排序序列,p为开始,r为结束
{
if p < r
q = [(p + r) / 2]; //应该是向下取整
MergeSort(A,p,q); //递归左子序列
MergeSort(A,q + 1,r); //递归右子序列
Merge(A,p,q,r);
}
时间复杂度:最好、最坏、平均都是O(nlog₂n)
在归并排序中对小数组采用插入排序,有的时候会更快
冒泡排序
循环遍历序列,每次当前子序列的最大(小)值交换到前面。“冒泡”
for i = 1 to A.length - 1
for j = A.length downto i + 1
if A[j] < A[j - 1]
enchange A[j] with A[j - 1]
时间复杂度:和插入排序一样
堆排序
堆,长得很像二叉树。分为最大堆和最小堆。下面的堆排序使用最大堆;最小堆常用于构造优先序列。
具体概念百度。(画图太麻烦)
将A[n](不符合最大堆的条件的某个元素)“逐级下降”,从而使A重新变成最大堆
伪代码如下:MaxHeapify(A,i) { //获取节点的两个子节点 l = left(i); r = right(i); //从左子节点和本节点之间选个最大的,获得他的标号 if l <= A.heapsize and A[l] > A[i] largest = l; else largest = i; //从当前最大和右子节点之间选最大的,获得它的标号 if r <= A.heapsize and A[r] > A[largest] largest = r; //如果最大的和原来的节点不一样,则交换元素,对这一次最大的节点调用这个函数 if largest != i exchange A[i] with A[largest] ManHeapify(A,largest) }
时间复杂度:树高h的节点O(h)。
将一个无序的数组构建成最大堆
关键:每一个叶节点都可以看成一个小堆
伪代码如下:BuildMaxHeap(A) { A.heapsize = A.length; for i = A.length / 2 downto 1 MaxHeapify(A,i); }
时间复杂度:O(n)。
堆排序
首先,将输入的无序数组通过BuildMaxHeap建立最大堆,A[1]是最大值。然后将最大堆的最后一个元素与A[1]交换,然后对A[1]调用MaxHeapify,将剩下的n-1个元素重新构建最大堆。此时,数组的第n个元素(最后一个元素)即为最大值。重复上述过程,依次将最大值放到后面。
伪代码:HeapSort(A) { BuildManHeap(A); for i = A.length downto 1 exchange A[1] with A[i]; A.heapsize = A.heapsize - 1; MaxHeapify(A,1); }
时间复杂度:O(nlog₂n)。
快速排序
很像归并排序,采用了分治的思想。
- 分解:将数组A[p,r]分成三部分,包括两个子数组(A[p,q-1]和A[q+1,r])和一个元素(A[r])。使得A[p,q-1]中的每一个元素都小于A[r],A[p,q-1]中的每一个元素都大于A[r]。
- 解决:递归的调用快速排序,对两个子数组进行排序。
- 合并:如果是原址排序,则不需要合并。(上面那个就是原址排序)
为了使算法的时间复杂度更平均,选取元素A[r]的时候采用随机选取
伪代码如下:
//主函数
QuickSort(A, p, r)
{
if p < r
q = RandomizedPartition(A,p,r);
QuickSort(A,p,q-1);
QuickSort(A,q+1,r);
}
//对数组进行随机划分
RandomizedPartition(A,p,r)
{
i = Random(p,r);
enchange A[r] with A[i];
return Partition(A,p,r);
}
//对数组进行划分的主要函数
Partition(A,p,r)
{
//将数组的最后一个元素认定为“被选召的元素”
x = A[r];
i = p - 1;
for j = p to r - 1
if A[j] <= x
i = i + 1;
exchange A[i] with A[j]
//上面那个循环结束后,前面(i指向结尾)元素小于x,后面元素大于x,最后一个元素是x
exchange A[i + 1] with A[r]; //交换x和大于x的元素数组里面的第一个
return i + 1; //返回“被选召的元素”的标号
}
时间复杂度:因为引入了随机数,所以最坏运行时间O(n²),期望运行时间O(nlog₂n)。
快速排序作为一个最为出名的排序,写完这个感觉可以到此为止了hehehe。。。
比较排序:在排序的最终结果中,各元素之间的次序决定于它们之间互相的比较。
任何比较排序最坏情况下都要经过O(nlog₂n)次比较,因此,归并排序和堆排序是渐进最优的,任何比较排序最多只是在常数因子上优于它们。
三种线性排序算法
一、计数排序
假设n个输入元素中每一个都是0到k区间中的一个整数,其中k为某个整数。
对于每一个输入元素x,确定小于x的元素个数,然后就能把它放到对应的位置上。要注意相同元素的放置。
输入数组A[1,n],A.length = n。还需要两个数组:B[1,n]存放排序的输出,C[0,k]提供临时存放空间。
伪代码如下:
CountintSort(A,B,k) //k是数组A中元素大小的界,或者说A中的元素都小于等于k
{
new C[0,k]; //存放0到k每个元素出现的个数
for i = 0 to k
C[i] = 0; //全部置0,表示还没给待排序的数组计数
for j = 1 to A.length
C[A[j]] ++; //待排序数组中,每个元素出现一次,相应的C[A[j]]加1
//做完上面,C[]数组中存储了每个元素出现的次数
for i = 1 to k
C[i] = C[i] + C[i - 1]
//做完上面,就统计了每个元素所对应的位置(通过将前面的加和)
for j = A.length downto 1 //倒序排序
B[C[A[j]]] = A[j]; //把元素A[j]放到B[]中,位置由C[]决定,此时的C[]中储存了每一个A[]元素的位置
C[A[j]] = C[A[j]] - 1; //每放一个元素,对应元素的位置减一。
}
时间复杂度:O(k+n),k为数组元素区间上限,n为数组元素数量
排序稳定: 具有相同值得元素在输出数组中的相对次序与它们在输入数组中的相对次序相同。
计数排序是稳定的。
二、基数排序
从最低位开始,对最低位排序,然后倒数第二位、倒数第三位。。。一直到最高位排序结束。这时所有的元素已排序。
这里,因为每一位都是0到10,所以可以使用上面的计数排序进行每一位的排序。快而且稳定
伪代码如下:
RadixSort(A,d)
{
for i = 1 to d //这里的d指的是最高位数,1是最低位
//用计数排序对数组A进行排序,基于第i位。
}
时间复杂度:O(d(n + k))。d是位数,n是元素个数,k是每一位的最大值
三、桶排序
假设输入数据均匀分布,例如由一个随机过程产生。桶排序把元素均匀、独立地分布在[0,1)区间上。
将[0,1)划分为n个相同大小的子区间,称为桶。然后将n个输入数分别放入各个桶中,然后对每个桶排序,然后遍历每个桶,将它们照次序输出。桶排序基于链表。
伪代码如下:
BucketSort(A)
{
n = A.length //数组中有几个元素,就设置几个桶
new B[0,n-1]; //创建桶数组B[]
for i = 0 to n-1
B[i] = new list(); //为B[]中的每个桶创建空链表
for i = 1 to n
insert A[i] to list B[ (int) (nA[i]) ] //将元素A[i]放入B[]桶中,通过乘以元素总数并取整,得到应该放入的桶的标号
for i = 0 to n - 1
对每个桶B[i]进行排序,通过插入排序
按B[0],B[1],B[2]...B[n-1]的顺序依次连接
}
时间复杂度:期望运行时间O(n)。
有点儿类似哈希表,通过计算来确定元素放在什么地方
这里放上一张表,表示了几种常用排序的复杂度
1、直接选择算法,当用链表实现的时候,是稳定的
2、归并排序空间复杂度一般来说是O(N)的,表中的O(1)也可以实现
总结
一般来说,几个主要的排序想起哪个用哪个。。。(?)