您的位置:首页 > 其它

线性非线性时间复杂度的9个排序算法整理

2016-10-16 23:23 323 查看
排序算法主划分成了线性和非线性,分享出整理的结果,大家一起学习。

一、线性排序算法

线性时间排序的时间复杂度可以看多是o(n),线性排序的代价往往是使用的额外空间大于n,是以时间换空间算法的典型。

1.计数排序

假设:有n个数的集合,而且n个数的范围都在0~k(k = O(n))之间。

运行时间:Θ(n+k)




待排序数组A如图2.1所示,需要辅助数组B(存储最后排序结果),数组C(存储元素的个数)。基于上述的假设,数组C的大小为k,C[i]表示数组A中元素i(0 <= i < k)的个数(如图2.2所示),为了保证计数排序的稳定性,数组C变化为图2.3,C[i]表示小于或者等于i的个数,是一个累计求和,用来还原到数组B,如果保序的话,从后往前找,例如先判断A[7]=1,则把它放到B[C[1]]对应的位置,然后将C[1]的值自减1。判断A[6],A[6]是0,放到对应的B[C[0]]的位置,然后C[0]的值减一,依次类推,直到把全部的A都安放到B中。代码如下:   

public class countSort {
//k是数组中最大的数,因为要建立长度为K的数组进行计数统计。
public static void CountingSort(int A[], intB[], int len, int k){
int CountArr[] = new int[k];
int i;
for (i = 0; i < k; i++){
CountArr[i] = 0;
}

for (i = 0; i < len; i++){
CountArr[A[i]]++;
}

for (i = 1; i < k; i++){
CountArr[i] += CountArr[i-1];
}

// 从右至左保证算法的稳定性
for (i = len-1; i >=0; i--){
B[CountArr[A[i]]-1] = A[i];
CountArr[A[i]]--;
}
}
public static int maxInt(int[] array){
int max=array[0];
for(int i=1;i<array.length;i++){
if(max<array[i]){
max=array[i];
}
}
return max;
}
public static void main(String args[]){
int A[]=new int[]{1,5,7,9,2,4,6,8,0,11};
int B[]=new int[10];
int max=maxInt(A);
CountingSort(A, B, A.length, max+1);
for(int i:B){
System.out.print(i+" ");
}
}
}

2.基数排序

基数排序:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

基数排序分为两种LSD和MSD。

LSD(Least significant digital):最低有效位优先,即从右向左开始排序。

MSD(Most significant digital):最高有效位优先,即从左往右开始排序。

以下是LSD方式的基数排序的伪代码

 

  public class RadixSort
{
public static void sort(int[] number, int d) //d表示最大的数有多少位
{
int k = 0;
int n = 1;
int m = 1; //控制键值排序依据在哪一位
int[][]temp = new int[10][number.length]; //数组的第一维表示可能的余数0-9
int[]order = new int[10]; //数组orderp[i]用来表示该位是i的数的个数
while(m <= d)
{
for(int i = 0; i < number.length; i++)
{
int lsd = ((number[i] / n) % 10);
temp[lsd][order[lsd]] = number[i];
order[lsd]++;
}
for(int i = 0; i < 10; i++)
{
if(order[i] != 0)
for(int j = 0; j < order[i]; j++)
{
number[k] = temp[i][j];
k++;
}
order[i] = 0;
}
n *= 10;
k = 0;
m++;
}
}
public static void main(String[] args)
{
int[]data =
{73, 22, 93, 43, 55, 14, 28, 65, 39, 81, 33, 100};
RadixSort.sort(data, 3);
for(int i = 0; i < data.length; i++)
{
System.out.print(data[i] + " ");
}
}
}

分析。外层循环中使用的是while(d--),也就是取决于数据的维数,对于一般的排序,可以看做是常量级别,除非像2进制并且具有很长的位数,位数大于元素的个数时,基数排序就不具有优势了。尽管while里面有放桶和重拾的循环,整体上,时间复杂度是o(n)的,因此总体可以看做o(n)。

 

二、非线性排序算法 (基数排序属于上一类)



1.直接插入排序:

将一个记录插入到已排序好的有序表中,从而得到一个新的记录数增1的有序表。即:先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。要点:设立哨兵,作为临时存储和判断数组边界之用。

public class insertSort {
publicstatic void print(int a[], int n ,int i){
System.out.println(i+":");;
for(intj= 0; j<8; j++){
System.out.print(a[j]+"");
}
System.out.println();
}
publicstatic void InsertSort(int a[], int n)
{
for(inti= 1; i<n; i++){
if(a[i]< a[i-1]){ //若第i个元素大于i-1元素,直接插入。小于的话,移动有序表后插入
intj= i-1;
int x = a[i]; //复制为哨兵,即存储待排序元素
a[i]= a[i-1]; //先后移一个元素
while(j>=0&&x< a[j]){ //查找在有序表的插入位置,
a[j+1]= a[j];
j--; //元素后移
}
a[j+1]= x; //插入到正确位置
}
print(a,n,i); //打印每趟排序的结果
}
}
publicstatic void main(String args[]){
inta[] = {3,1,5,7,2,4,9,6};
InsertSort(a,8);
print(a,8,8);
}
}

在已经排好序的情况下,时间复杂度为o(n)(所以在基本有序情况下使用这种排序是比较好的),因为只需要执行数组长度-1次的循环,每次循环中只比较后一个和前一个数值的大小。最坏的情况是倒序排列的情况下,循环n-1次,每次循环中都要进行比较后移和插入,n-1次插入加上从2倍的从1到n-1的累加时间复杂度。最坏情况下时间复杂度是o(n2)。平均时间复杂度是o(n2)。

只有一个变量存储哨兵,空间复杂度为o(1)。

只有要插入的数小于前面已排序好的数才移动数组,大于等于时数组位置不动,因此是稳定的算法。

2、希尔排序

是1959 年由D.L.Shell 提出来的,相对直接排序有较大的改进。希尔排序又叫缩小增量排序

基本思想:

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序(与上面说到基本有序时直接插入的时间复杂度o(n)相呼应)。

操作方法:

选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;

按增量序列个数k,对序列进行k 趟排序;

每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

们简单处理增量序列:增量序列d = {n/2 ,n/4, n/8 .....1} n为要排序数的个数

即:先将要排序的一组记录按某个增量d(n/2,n为要排序数的个数)分成若干组子序列,每组中记录的下标相差d.对每组中全部元素进行直接插入排序,然后再用一个较小的增量(d/2)对它进行分组,在每组中再进行直接插入排序。继续不断缩小增量直至为1,最后使用直接插入排序完成排序。

public class shellSort {
publicstatic void print(int a[], int n ,int i){
System.out.println(i+":");
for(intj= 0; j<8; j++){
System.out.print(a[j]+"");
}
System.out.println();
}
/**
* 直接插入排序的一般形式
*
* @param int dk 缩小增量,如果是直接插入排序,dk=1
*
*/

publicstatic void ShellInsertSort(int a[], int n, int dk)
{
for(inti= dk; i<n;i++){
if(a[i]< a[i-dk]){ //若第i个元素大于i-1元素,直接插入。小于的话,移动有序表后插入
intj = i-dk;
intx = a[i]; //复制为哨兵,即存储待排序元素
a[i]= a[i-dk]; //首先后移一个元素
while(j>=0&&x< a[j]){ //查找在有序表的插入位置
a[j+dk] = a[j];
j-= dk; //元素后移
}
a[j+dk] = x; //插入到正确位置
}
}
print(a,n, dk);
}

/**
* 先按增量n/2为要排序数的个数进行希尔排序
*
*/
publicstatic void shellSort(int a[], int n){

intdk = n/2;
while(dk >= 1 ){
ShellInsertSort(a,n, dk);
dk= dk/2;
}
}
publicstatic void main(String args[]){
inta[] = {3,1,5,7,2,4,9,6};
//ShellInsertSort(a,8,1);//直接插入排序
shellSort(a,8); //希尔插入排序
print(a,8,8);
}

}


 

3.选择排序:

基本思想:在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。

简单选择排序的示例:

 


操作方法:

第一趟,从n 个记录中找出关键码最小的记录与第一个记录交换;

第二趟,从第二个记录开始的n-1 个记录中再选出关键码最小的记录与第二个记录交换;

以此类推.....

第i 趟,则从第i 个记录开始的n-i+1 个记录中选出关键码最小的记录与第i 个记录交换,

直到整个序列按关键码有序

public class selectSort {
staticvoid print(int a[], int n ,int i){
System.out.print("第"+i+1+"趟 : ");
for(intj= 0; j<8; j++){
System.out.print(a[j]+" ");
}
System.out.println();
}
/**
* 数组的最小值
*
* @return int 数组的键值
*/
staticint SelectMinKey(int a[], int n, int i)
{
intk = i;
for(intj=i+1 ;j< n; ++j) {
if(a[k]> a[j]) k = j;
}
returnk;
}

/**
* 选择排序
*
*/
staticvoid selectSort(int a[], int n){
intkey, tmp;
for(inti = 0; i< n; ++i) {
key= SelectMinKey(a, n,i); //选择最小的元素
if(key!= i){
tmp= a[i]; a[i] = a[key]; a[key] = tmp; //最小元素与第i位置元素互换
}
print(a, n , i);
}
}
publicstatic void main(String args[]){
inta[] = {3,1,5,7,2,4,9,6};
System.out.print("初始值:");
for(intj= 0; j<8; j++){
System.out.print(a[j]+"");
}
System.out.println();
selectSort(a,8);
print(a,8,8);
}
}

4.堆排序

是一种树形选择排序,是对直接选择排序的有效改进。

基本思想:

堆顶元素(即第一个元素)必为最小项(小顶堆)。

若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的。如:

(a)大顶堆序列:(96, 83,27,38,11,09)

  (b)  小顶堆序列:(12,36,24,85,47,30,53,91)

 


初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序。

因此,实现堆排序需解决两个问题:

1. 如何将n 个待排序的数建成堆;

2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。

首先讨论第二个问题:输出堆顶元素后,对剩余n-1元素重新建成堆的调整过程。

调整小顶堆的方法:

1)设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶((最后一个元素与堆顶进行交换),堆被破坏,其原因仅是根结点不满足堆的性质。

2)将根结点与左、右子树中较小元素的进行交换。

3)若与左子树交换:如果左子树堆被破坏,即左子树的根结点不满足堆的性质,则重复方法(2).

4)若与右子树交换,如果右子树堆被破坏,即右子树的根结点不满足堆的性质。则重复方法(2).

5)继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。

称这个自根结点到叶子结点的调整过程为筛选。如图:



再讨论对n 个元素初始建堆的过程。

建堆方法:对初始序列建堆的过程,就是一个反复进行筛选的过程。

1)n 个结点的完全二叉树,则最后一个结点是第 个结点的子树。

2)筛选从第 个结点为根的子树开始,该子树成为堆。

3)之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。

如图建堆初始过程:无序序列:(49,38,65,97,76,13,27,49)

  

                            

package ff;
public class HeapSort{
staticvoid print(int a[], int n){
for(intj= 0; j<n; j++){
System.out.print(a[j]+"");
}
System.out.println();
}

/**
* 已知H[s…m]除了H[s] 外均满足堆的定义
* 调整H[s],使其成为大顶堆.即将对第s个结点为根的子树筛选,
*
* @param H是待调整的堆数组
* @param s是待调整的数组元素的位置
* @param length是数组的长度
*
*/
staticvoid HeapAdjust(int H[],int s, int length)
{
inttmp = H[s];
intchild = 2*s+1; //左孩子结点的位置。(i+1 为当前调整结点的右孩子结点的位置)
while (child < length) {
if(child+1<length && H[child]<H[child+1]) { // 如果右孩子大于左孩子(找到比当前待调整结点大的孩子结点)
++child;
}
if(H[s]<H[child]){ // 如果较大的子结点大于父结点
H[s]= H[child]; // 那么把较大的子结点往上移动,替换它的父结点
s= child; // 重新设置s ,即待调整的下一个结点的位置
child= 2*s+1;
} else { // 如果当前待调整结点大于它的左右孩子,则不需要调整,直接退出
break;
}
H[s]= tmp; // 当前待调整的结点放到比其大的孩子结点位置上
}
print(H,length);
}

/**
* 初始堆进行调整
* 将H[0..length-1]建成堆
* 调整完之后第一个元素是序列的最小的元素
*/
staticvoid BuildingHeap(int H[], int length)
{
//最后一个有孩子的节点的位置i= (length -1) / 2
for(int i = (length -1) / 2 ; i >= 0; --i)
HeapAdjust(H,i,length);
}
/**
* 堆排序算法
*/
staticvoid HeapSort(int H[],int length)
{
//初始堆
BuildingHeap(H,length);
//从最后一个元素开始对序列进行调整
for(int i = length - 1; i > 0; --i)
{
//交换堆顶元素H[0]和堆中最后一个元素
inttemp = H[i]; H[i] = H[0]; H[0] = temp;
//每次交换堆顶元素和堆中最后一个元素之后,都要对堆进行调整
HeapAdjust(H,0,i);
}
}

publicstatic void main(String args[]){
intH[] = {3,1,5,7,2,4,9,6,10,8};
System.out.print("初始值:");
print(H,10);
/*
*堆排序首先要建堆,建堆的过程是从(length-1)/2到0进行堆调整。复杂度是就是长度。每次的堆排序都是循环。循环是按照2倍递增,因此这里肯定涉及到log2n的问题。
*其次是堆的排序,从n-1到0,进行调整么次都是log2n进行变化的。
*/
HeapSort(H,10);
//selectSort(a,8);
System.out.print("结果:");
print(H,10);

}
}

设树深度为k,

。从根到叶的筛选,元素比较次数至多2(k-1)次,交换记录至多k 次。

所以,在建好堆后,排序过程中的筛选次数不超过下式:        

                       而建

堆时的比较次数不超过4n 次,因此堆排序最坏情况下,时间复杂度也为:O(nlogn )。

 

 

 

 

 

 

5.冒泡排序(Bubble Sort)

基本思想:

在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。

冒泡排序的示例:

 


public class BubbleSort {
staticvoid bubbleSort(int a[], int n){
for(int i =0 ; i< n-1; ++i) {
for(int j = 0; j < n-i-1; ++j){
if(a[j] > a[j+1])
{
int tmp = a[j] ; a[j] = a[j+1]; a[j+1] = tmp;
}
}
}
}
publicstatic void main(String args[]){
int[]a=new int[]{2,4,3,7,6,1};
bubbleSort(a,a.length);
for(inti:a){
System.out.print(i+"");
}
}
}

冒泡算法的改进:设置一标志性变量pos,用于记录每趟排序中最后一次进行交换的位置。由于pos位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到pos位置即可。或者可以设置一个change的标志位,标志是否发生变化,当change不发生变化的时候表示已经排好序,则无需比对。

 

 

6.快速排序(Quick Sort)

基本思想:

1)选择一个基准元素,通常选择第一个元素或者最后一个元素,

2)通过一趟排序讲待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的 元素值比基准值大。

3)此时基准元素在其排好序后的正确位置

4)然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。

快速排序的示例:

(a)一趟排序的过程:



(b)排序的全过程



算法的实现:

public class exchangeSort {
staticvoid print(int a[], int n){
for(int j= 0; j<n; j++){
System.out.print(a[j]+" ");
}
System.out.println();;
}

staticvoid swap(int[] a,int noa,int nob)
{
int tmp = a[noa];
a[noa]=a[nob];
a[nob] = tmp;
}

staticint partition(int a[], int low, int high)
{
int privotKey = a[low]; //基准元素
while(low < high){ //从表的两端交替地向中间扫描
while(low < high && a[high] >= privotKey)--high; //从high 所指位置向前搜索,至多到low+1位置。将比基准元素小的交换到低端
swap(a,low,high);
while(low < high && a[low] <= privotKey )++low;
swap(a,low,high);
}
print(a,10);
return low;
}

staticvoid quickSort(int a[], int low, int high){
if(low < high){
int privotLoc = partition(a, low, high); //将表一分为二
quickSort(a, low, privotLoc -1); //递归对低子表递归排序
quickSort(a, privotLoc + 1, high); //递归对高子表递归排序
}
}

publicstatic void main(String args[]){
int a[] = {3,1,5,7,2,4,9,6,10,8};
System.out.print("初始值:");
print(a,10);
quickSort(a,0,9);
System.out.println("结果:");
print(a,10);
}
}

分析:

快速排序是通常被认为在同数量级(O(nlog2n))的排序方法中平均性能最好的。但若初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。快速排序是一个不稳定的排序方法。

快速排序采用到了分治的思想,下面是主定理的基本内容,主定理可以解释分治算法时间复杂度的问题。



7. 归并排序(Merge Sort)

基本思想:

归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

归并排序示例:

 


 

python两路归并的递归算法,例如5个元素的排序,除2后分成前2和后3两组数据进行排序。前2再分成1和2,数组长度均满足1,可以返回对应下标的数据lists[0],lists[1],送入merge函数进行归并排序。后3个可以分成3和(4,5);3可以返回lists[2],但(4,5)分成4和5,所以lists[4]和lists[5]先merge,返回的的lists(这里有两个元素)与lists[2]做merge运算。最后再将1和2merge后的结果与3,4,5merge后的结果进行merge。这就是5个元素的merge过程。

merge实现的过程是排序,这里存入的是新的list-----result。由于24小于23,所以23先加到result,继续循环,24被加入result,循环打破,最后加入right剩余的元素45。假如left是22,第一步比较就小于right,会被加入到result,直接跳出循环,而后,right的23,45会被追加到result中。

def MergeSort(lists):
if len(lists) <= 1:
return lists
num = int( len(lists)/2 )
left = MergeSort(lists[:num])
right = MergeSort(lists[num:])
return Merge(left, right)
def Merge(left,right):
r, l=0, 0
result=[]
while l<len(left) andr<len(right):
if left[l] < right[r]:
result.append(left[l])
l += 1
else:
result.append(right[r])
r += 1
result += right[r:]
result+= left[l:]
return result
print MergeSort([7, 90, 24, 23, 45])
个人感觉,python的代码更好理解,归并排序用python来实现不错。

非线性算法的复杂度表



 

 
参考地址:http://blog.csdn.net/hguisu/article/details/7776068

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: