十大常用排序算法(java实现)(转)

转:

http://blog.csdn.net/qq_21688757/article/details/53749198

http://blog.csdn.net/u014682691/article/details/50787366

十大常用排序算法(java实现)


- 【对比分析图】首先,我们先来对比分析一下这十大排序算法的特点: 
这里写图片描述


这里写图片描述

(一).冒泡排序(优化)

          1)算法简介

        冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

2)算法描述

      1、比较相邻的元素。如果第一个比第二个大,就交换他们两个。

      2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。

      3、针对所有的元素重复以上的步骤,除了最后一个。

      4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

       冒泡排序是与插入排序拥有相等的执行时间,但是两种法在需要的交换次数却很大地不同。在最坏的情况,冒泡排序需要O(n^2)次交换,而插入排序只要最多O(n)交换。冒泡排序的实现(类似下面)通常会对已经排序好的数列拙劣地执行(O(n^2)),而插入排序在这个例子只需要O(n)个运算。因此很多现代的算法教科书避免使用冒泡排序,而用插入排序取代之。冒泡排序如果能在内部循环第一次执行时,使用一个旗标来表示有无需要交换的可能,也有可能把最好的复杂度降低到O(n)。在这个情况,在已经排序好的数列就无交换的需要。若在每次走访数列时,把走访顺序和比较大小反过来,也可以稍微地改进效率。有时候称为往返排序,因为算法会从数列的一端到另一端之间穿梭往返。

最差时间复杂度

O(n^2)

最优时间复杂度

O(n)

平均时间复杂度

O(n^2)

最差空间复杂度

总共O(n),需要辅助空间O(1)

3)算法图解

图解:

4)代码


【题目】对于一个int数组,请编写一个冒泡排序算法,对数组元素排序。 
给定一个int数组A及数组的大小n,请返回排序后的数组。

  • 冒泡排序,顾名思义,从下往上遍历,每次遍历往上固定一个最小值
  • 加一个标志位,当某一趟冒泡排序没有元素交换时,则冒泡结束,元素已经有序,可以有效的减少冒泡次数。
import java.util.*;

public class BubbleSort {
    public int[] bubbleSort(int[] A, int n) {
        //冒泡排序:从后往前(从下往上)就像冒泡一样
        //用flag作为标记,标记数组是否已经排序完成
        boolean flag = true;
        //固定左边的数字
        for(int i=0; i<n-1&flag; i++){
            flag = false;
            //从后面(下面)往前(上)遍历
            for(int j=n-2;j>=i;j--){

                if(A[j]>A[j+1]){
                    swap(A,j,j+1);
                    flag = true;
                }
            }
        }

        return A;

    }
    //数组是按引用传递,在函数中改变数组起作用
    private void swap(int[] A,int i,int j){
        int temp = A[i];
        A[i] = A[j];
        A[j] = temp;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

5)笔试面试例题

例题1

对于整数序列1009998321,如果将它完全倒过来,分别用冒泡排序,它们的比较次数和交换次数各是多少?
     答:冒泡排序的比较和交换次数将最大,都是1+2++n-1=n(n-1)/250×99=4545


(二).简单选择排序

1)算法简介

       选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

2)算法描述和分析

       n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果:

       1、初始状态:无序区为R[1..n],有序区为空。

       2、第i趟排序(i=1,2,3...n-1)

第i趟排序开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。

       3、前n-1趟结束,数组有序化了

        选择排序的交换操作介于0(n-1)次之间。选择排序的比较操作为n(n-1)/2次之间。选择排序的赋值操作介于03(n-1)次之间。比较次数O(n^2),比较次数与关键字的初始状态无关,总的比较次数N=(n-1)+(n-2)+...+1=n*(n-1)/2。 交换次数O(n),最好情况是,已经有序,交换0次;最坏情况是,逆序,交换n-1次。 交换次数比冒泡排序少多了,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。

最差时间复杂度

О(n²)

最优时间复杂度

О(n²)

平均时间复杂度

О(n²)

最差空间复杂度

О(n) total, O(1)

3)算法图解、flash演示、视频演示

图解:

4)算法代码


【题目】 对于一个int数组,请编写一个选择排序算法,对数组元素排序。 
给定一个int数组A及数组的大小n,请返回排序后的数组。

  • 1.【初始升序】:交换0次,时间复杂度为o(n); 【初始降序】:交换n-1次,时间复杂度为o(n^2)。
  • 【特点】:交换移动数据次数少,比较次数多。
import java.util.*;
/**

**/
public class SelectionSort {
    public int[] selectionSort(int[] A, int n) {
       //简单选择排序算法,排序结果为递增数组
        //记录最小下标值
        int min=0;
        //固定左边的数字
        for(int i=0; i<A.length-1;i++){
            min = i;
            //找到下标i开始后面的最小值
            for(int j=i+1;j<A.length;j++){

                 if(A[min]>A[j]){
                     min = j;
                 }
            }
           //确保稳定排序,数值相等就不用交换
            if(i!=min){
                swap(A,i,min);
            }
        }

        return A;

    }

    //数组是按引用传递,在函数中改变数组起作用
    private void swap(int[] A,int i,int j){
        int temp = A[i];
        A[i] = A[j];
        A[j] = temp;
    }

}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

5)笔试面试例题

例题1

在插入和选择排序中,若初始数据基本正序,则选用 插入排序(到尾部)   ;若初始数据基本反序,则选用   选择排序     。

例题2

 下述几种排序方法中,平均查找长度(ASL)最小的是
插入排序      .快速排序        C归并排序       D选择排序


(三).直接插入排序

1)算法简介

        插入排序(Insertion Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

2)算法描述和分析

    一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

    1、从第一个元素开始,该元素可以认为已经被排序

    2、取出下一个元素,在已经排序的元素序列中从后向前扫描

    3、如果该元素(已排序)大于新元素,将该元素移到下一位置

    4、重复步骤3,直到找到已排序的元素小于或者等于新元素的位置

    5、将新元素插入到该位置后

    6、重复步骤2~5

        如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数减去(n-1)次。平均来说插入排序算法复杂度为O(n^2)。因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。

3)算法图解

图解:


4)算法代码


【题目】对于一个int数组,请编写一个插入排序算法,对数组元素排序。 
给定一个int数组A及数组的大小n,请返回排序后的数组。

import java.util.*;

public class InsertionSort {
    public int[] insertionSort(int[] A, int n) {
      //用模拟插入扑克牌的思想
        //插入的扑克牌
        int i,j,temp;
        //已经插入一张,继续插入
        for(i=1;i<n;i++){

            temp = A[i];
            //把i前面所有大于要插入的牌的牌往后移一位,空出一位给新的牌
            for(j=i;j>0&&A[j-1]>temp;j--){
                A[j] = A[j-1];
            }
            //把空出来的一位填满插入的牌
            A[j] = temp;

        }
        return A;


    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

5)笔试面试例题

例题1、

下列排序算法中最坏复杂度不是n(n-1)/2的是 D

A.快速排序     B.冒泡排序   C.直接插入排序   D.堆排序


(四).希尔排序

1)算法简介

希尔排序,也称递减增量排序算法,因DL.Shell于1959年提出而得名,是插入排序的一种高速而稳定的改进版本。

2)算法描述

    1、先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。

    2、所有距离为d1的倍数的记录放在同一个组中,在各组内进行直接插入排序。

    3、取第二个增量d2<d1重复上述的分组和排序,

    4、直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。

          希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n^2),而Hibbard增量的希尔排序的时间复杂度为O(N^(5/4)),但是现今仍然没有人能找出希尔排序的精确下界。

3)算法图解、flash演示、视频演示

图解:






4)算法代码


【题目】对于一个int数组,请编写一个希尔排序算法,对数组元素排序。 
给定一个int数组A及数组的大小n,请返回排序后的数组。保证元素小于等于2000。

  • 基本思想:算法先将要排序的一组数按某个增量d(n/2,n为要排序数的个数)分成若干组,每组中记录的下标相差d.对每组中全部元素进行直接插入排序,然后再用一个较小的增量(d/2)对它进行分组,在每组中再进行直接插入排序。当增量减到1时,进行直接插入排序后,排序完成。

  • 希尔排序法(缩小增量法) 属于插入类排序,是将整个无序列分割成若干小的子序列分别进行插入排序的方法。


import java.util.*;

public class ShellSort {
    public int[] shellSort(int[] A, int n) {
        //要插入的纸牌
        int temp,j,i;
        //设定增量D,增量D/2逐渐减小
        for(int D = n/2;D>=1;D=D/2){

            //从下标d开始,对d组进行插入排序
            for(j=D;j<n;j++){

                temp = A[j];
                for(i=j;i>=D&&A[i-D]>temp;i-=D){
                    A[i]=A[i-D];
                }

                A[i]=temp;
            }

        }

        return A;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

5)笔试面试例题

例题1、

写出希尔排序算法程序,并说明最坏的情况下需要进行多少次的比较和交换。

    程序略,需要O(n^2)次的比较

例题2、

设要将序列(Q, H, C, Y, P, A, M, S, R, D, F, X)中的关键码按字母序的升序重新排列,则:

冒泡排序一趟扫描的结果是       H, C, Q, P, A, M, S, R, D, F, X ,Y      ;

初始步长为4的希尔(shell)排序一趟的结果是   P, A, C, S, Q, D, F, X , R, H,M, Y     ;

二路归并排序一趟扫描的结果是   H, Q, C, Y,A, P, M, S, D, R, F, X   ;

快速排序一趟扫描的结果是     F, H, C, D, P, A, M, Q, R, S, Y,X     ;

堆排序初始建堆的结果是   A, D, C, R, F, Q, M, S, Y,P, H, X   。


(五).堆排序

1)算法简介

        堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

2)算法描述

       我们这里介绍几个问题,一步步推到堆排序的算法。

1、什么是堆?

      我们这里提到的堆一般都指的是二叉堆,它满足二个特性:

           1---父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。

           2---每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。

     如下为一个最小堆(父结点的键值总是小于任何一个子节点的键值)

2、什么是堆调整(Heap Adjust)

        这是为了保持堆的特性而做的一个操作。对某一个节点为根的子树做堆调整,其实就是将该根节点进行“下沉”操作(具体是通过和子节点交换完成的),一直下沉到合适的位置,使得刚才的子树满足堆的性质

       例如对最大堆的堆调整我们会这么做:

              1、在对应的数组元素A[i], 左孩子A[LEFT(i)], 和右孩子A[RIGHT(i)]中找到最大的那一个,将其下标存储在largest中。

              2、如果A[i]已经就是最大的元素,则程序直接结束。

              3、否则,i的某个子结点为最大的元素,将A[largest]与A[i]交换。

              4、再从交换的子节点开始,重复1,2,3步,直至叶子节点,算完成一次堆调整。

       这里需要提一下的是,一般做一次堆调整的时间复杂度为log(n)

       如下为我们对4为根节点的子树做一次堆调整的示意图,可帮我们理解。

3、如何建堆

         建堆是一个通过不断的堆调整,使得整个二叉树中的数满足堆性质的操作。在数组中的话,我们一般从下标为n/2的数开始做堆调整,一直到下标为0的数(因为下标大于n/2的数都是叶子节点,其子树已经满足堆的性质了)。下图为其一个图示:


        很明显,对叶子结点来说,可以认为它已经是一个合法的堆了即2060, 65, 4, 49都分别是一个合法的堆。只要从A[4]=50开始向下调整就可以了。然后再取A[3]=30A[2] = 17A[1] = 12A[0] = 9分别作一次向下调整操作就可以了。

4、如何进行堆排序

        堆排序是在上述3中对数组建堆的操作之后完成的。

        数组储存成堆的形式之后,第一次将A[0]A[n - 1]交换,再对A[0n-2]重新恢复堆。第二次将A[0]A[n-2]交换,再对A[0n-3]重新恢复堆,重复这样的操作直到A[0]A[1]交换。由于每次都是将最小的数据并入到后面的有序区间,故操作完成后整个数组就有序了。

如下图所示:




最差时间复杂度

O(n log n)

最优时间复杂度

O(n log n)

平均时间复杂度

O(n log n)

最差空间复杂度

 O(n)

3)算法图解


4)算法代码


【题目】对于一个int数组,请编写一个堆排序算法,对数组元素排序。 
给定一个int数组A及数组的大小n,请返回排序后的数组。

  • 【堆】 1.堆是完全二叉树 2.大顶堆:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆。3.小顶堆:每个结点的值都大于或等于其左右孩子结点的值,称为小顶堆。
  • 【完全二叉树性质5】:如果i>1,则双亲是结点[i/2]。也就是说下标i与2i和2i+1是双亲子女关系。 当排序对象为数组时,下标从0开始,所以下标 i 与下标 2i+1和2i+2是双亲子女关系。
import java.util.*;

public class HeapSort {
    public int[] heapSort(int[] A, int n) {
        //堆排序算法

        int i;
        //先把A[]数组构建成一个大顶堆。
        //从完全二叉树的最下层最右边的非终端结点开始构建。
        for(i=n/2-1;i>=0;i--){
            HeapAdjust(A,i,n);
        }

        //开始遍历
        for(i=n-1;i>0;i--){
            swap(A,0,i);
            //每交换一次得到一个最大值然后丢弃
            HeapAdjust(A,0,i);
        }
        return A;

    }
    //A[i]代表的是下标为i的根结点
    private void HeapAdjust(int[] A,int i,int n){
        //【注意】这里下标从0开始
        int temp;
        //存储根结点
        temp = A[i];
        //沿根结点的左右孩子中较大的往下遍历,由于完全二叉树特性 i的左子节点2i+1  i的右子节点2i+2
        for(int j=2*i+1;j<n;j=j*2+1){

            if(j<n-1&&A[j]<A[j+1]){
                ++j;
            }

            if(temp>=A[j]){
                break;
            }
            //将子节点赋值给根结点
            A[i] = A[j];
            //将子节点下标赋给i
            i = j;
        }
        //将存储的根结点的值赋给子节点
        A[i] = temp;

    }
    private void swap(int[] A,int i,int j){
        int temp = A[i];
        A[i]=A[j];
        A[j] = temp;

    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

5)笔试面试题  

设计一个数据结构,其中包含两个函数,1.插入一个数字,2.获得中数。并估计时间复杂度。

        使用大顶堆和小顶堆存储。

  使用大顶堆存储较小的一半数字,使用小顶堆存储较大的一半数字。

  插入数字时,在O(logn)时间内将该数字插入到对应的堆当中,并适当移动根节点以保持两个堆数字相等(或相差1)。

  获取中数时,在O(1)时间内找到中数。


(六).归并排序算法

1)算法简介

        归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。

       将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

2)算法描述

    归并排序具体算法描述如下(递归版本):

    1、Divide: 把长度为n的输入序列分成两个长度为n/2的子序列。

    2、Conquer: 对这两个子序列分别采用归并排序。

    3、Combine: 将两个排序好的子序列合并成一个最终的排序序列。

    归并排序的效率是比较高的,设数列长为N将数列分开成小数列一共要logN步,每步都是一个合并有序数列的过程,时间复杂度可以记为O(N),故一共为O(N*logN)。因为归并排序每次都是在相邻的数据中进行操作,所以归并排序在O(N*logN)的几种排序方法(快速排序,归并排序,希尔排序,堆排序)也是效率比较高的。

3)算法图解、flash演示、视频演示

图解:


4)算法代码


【题目】对于一个int数组,请编写一个归并排序算法,对数组元素排序。 
给定一个int数组A及数组的大小n,请返回排序后的数组。

  • 先用递归方法,默认排序方法为2路归并排序。看下图应该能立即理解: 
    这里写图片描述
  • 先使每个子序列有序,再将两个已经排序的序列合并成一个序列的操作。若将两个有序表合并成一个有序表,称为二路归并。
import java.util.*;

public class MergeSort {
    public int[] mergeSort(int[] A, int n) {
       //归并排序,递归做法,分而治之

        mSort(A,0,n-1);
        return A;
    }

    private void mSort(int[] A,int left,int right){
        //分而治之,递归常用的思想,跳出递归的条件
        if(left>=right){
            return;
        }
        //中点
        int mid = (left+right)/2;
        //有点类似后序遍历!
        mSort(A,left,mid);
        mSort(A,mid+1,right);

        merge(A,left,mid,right);



    }

    //将左右俩组的按序子序列排列成按序序列
    private void merge(int[] A,int left,int mid,int rightEnd){
        //充当tem数组的下标
        int record = left;
        //最后复制数组时使用
        int record2 = left;
        //右子序列的开始下标
        int m =mid+1;
        int[] tem = new int[A.length];

        //只要left>mid或是m>rightEnd,就跳出循环
        while(left<=mid&&m<=rightEnd){

            if(A[left]<=A[m]){

                tem[record++]=A[left++];
            }else{
                tem[record++]=A[m++];
            }

        }
        while(left<=mid){
            tem[record++]=A[left++];
        }
        while(m<=rightEnd){
            tem[record++]=A[m++];
        }
        //复制数组
        for( ;record2<=rightEnd;record2++){
            A[record2] = tem[record2];
        }

    }

}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

5)笔试面试题

例题1

有10个文件,每个文件1G,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。要求你按照query的频度排序。

        1hash映射:顺序读取10个文件,按照hash(query)%10的结果将query写入到另外10个文件(记为)中。这样新生成的文件每个的大小大约也1G(假设hash函数是随机的)。

        2hash统计:找一台内存在2G左右的机器,依次对用hash_map(query, query_count)来统计每个query出现的次数。注:hash_map(query,query_count)是用来统计每个query的出现次数,不是存储他们的值,出现一次,则count+1

        3堆/快速/归并排序:利用快速//归并排序按照出现次数进行排序。将排序好的query和对应的query_cout输出到文件中。这样得到了10个排好序的文件(记为)。对这10个文件进行归并排序(内排序与外排序相结合)。



(七).快速排序算法

1)算法简介

       快速排序是由东尼·霍尔所发展的一种排序算法。其基本思想是基本思想是,通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

2)算法描述和分析

快速排序使用分治法来把一个串(list)分为两个子串行(sub-lists)。

步骤为:

     1、从数列中挑出一个元素,称为 "基准"(pivot),

     2、重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。

     3、递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

    递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

算法伪代码描述:

function quicksort(q)

     var list less, pivotList, greater

     if length(q) ≤ 1 {

         return q

     } else {

         select a pivot value pivot from q

         for each x in q except the pivot element

             if x < pivot then add x to less

             if x ≥ pivot then add x to greater

         add pivot to pivotList

         return concatenate(quicksort(less), pivotList, quicksort(greater))

     }

平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

最差时间复杂度

O(n^2)

最优时间复杂度

O(n log n)

平均时间复杂度

O(n log n)

最差空间复杂度

根据实现的方式不同而不同

3)算法图解

图解:

        快速排序会递归地进行很多轮,其中每一轮称之为快排的partition算法,即上述算法描述中的第2步,非常重要,且在各种笔试面试中用到该思想的算法题层出不穷,下图为第一轮的partition算法的一个示例。

4)算法代码


【题目】对于一个int数组,请编写一个快速排序算法,对数组元素排序。 
给定一个int数组A及数组的大小n,请返回排序后的数组。

  • 【基本思想】:快速排序(Quicksort)是对冒泡排序的一种改进,使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。

  • 【步骤为】

  • 从数列中挑出一个元素,称为”枢轴”(pivot)。
  • 重新排序数列,所有元素比枢轴值小的摆放在基准前面,所有元素比枢轴值大的摆在枢轴的后面(相同的数可以到任一边)。在这个分区结束之后,该枢轴就处于数列的中间位置。这个称为分区(partition)操作。
  • 递归地(recursive)把小于枢轴值元素的子数列和大于枢轴值元素的子数列排序。
import java.util.*;

public class QuickSort {
    public int[] quickSort(int[] A, int n) {
        //快速排序

        qSort(A,0,n-1);

        return A;

    }
    public void qSort(int[] A,int left,int right){

         //枢轴
        int pivot;
        if(left<right){

            pivot = partition(A,left,right);

            qSort(A,left,pivot-1);
            qSort(A,pivot+1,right);

        }      

    }

    //优化选取一个枢轴,想尽办法把它放到一个位置,使它左边的值都比它小,右边的值都比它大
    public int partition(int[] A,int left,int right){

        //优化选取枢轴,采用三数取中的方法
        int pivotKey = median3(A,left,right);
        //从表的俩边交替向中间扫描
        //枢轴用pivotKey给备份了
        while(left<right){
           while(left<right&&A[right]>=pivotKey){
               right--;
           }
            //用替换方式,因为枢轴给备份了,多出一个存储空间
            A[left]=A[right];
           while(left<right&&A[left]<=pivotKey){
               left++;
           }
           A[right]=A[left];

        }

        //把枢轴放到它真正的地方
        A[left]=pivotKey;
        return left;
    }
    //三数取中
    public int median3(int[] A,int left,int right){

        int mid=(right-left)/2;
        if(A[left]>A[right]){
            swap(A,left,right);
        }
        if(A[mid]>A[left]){
            swap(A,mid,left);
        }
        if(A[mid]>A[right]){
            swap(A,mid,right);
        }

        return A[left];
    }

    public void swap(int[] A,int i,int j){
        int temp =A[i];
        A[i]=A[j];
        A[j]=temp;
    }



}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 【快排效率】
  • 时间性能取决于递归的深度,可以用递归树来描述递归算法的执行情况!最优情况下,partition每次划分均匀,排序n个数值,则递归树的深度为[logn]+1,第一次partition需要对整个数组扫描一遍,做n次比较,第二次对一半扫描。所以最优时间复杂度 Ο(n log n) ,最差时间复杂度 Ο(n^2) ,平均时间复杂度Ο(n log n) 。

  • 递归导致栈空间的使用,最好空间复杂度为Ο(log n),最差空间复杂度Ο(n)。


(八).桶排序算法 
【桶排序的步骤】

  • 设置一个定量的数组当作空桶子。
  • 寻访序列,并且把项目一个一个放到对应的桶子去。
  • 对每个不是空的桶子进行排序。
  • 从不是空的桶子里把项目再放回原来的序列中。

1)算法简介

        桶排序 (Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)

       桶排序是稳定的,且在大多数情况下常见排序里最快的一种,比快排还要快,缺点是非常耗空间,基本上是最耗空间的一种排序算法,而且只能在某些情形下使用。

2)算法描述和分析

       桶排序具体算法描述如下:

      1、设置一个定量的数组当作空桶子。

      2、寻访串行,并且把项目一个一个放到对应的桶子去。

      3、对每个不是空的桶子进行排序。

      4、从不是空的桶子里把项目再放回原来的串行中。

       桶排序最好情况下使用线性时间O(n),很显然桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为 其它部分的时间复杂度都为O(n);很显然,桶划分的越小,各个桶之间的数据越少,排 序所用的时间也会越少。但相应的空间消耗就会增大。

       可以证明,即使选用插入排序作为桶内排序的方法,桶排序的平均时间复杂度为线性。 具体证明,请参考算法导论。其空间复杂度也为线性。

3)算法图解

图解

5)笔试面试题

例题1

一年的全国高考考生人数为500 万,分数使用标准分,最低100 ,最高900 ,没有小数,你把这500 万元素的数组排个序。

        对500W数据排序,如果基于比较的先进排序,平均比较次数为O(5000000*log5000000)≈1.112亿。但是我们发现,这些数据都有特殊的条件:  100=<score<=900。那么我们就可以考虑桶排序这样一个投机取巧的办法、让其在毫秒级别就完成500万排序。

       创建801(900-100)个桶。将每个考生的分数丢进f(score)=score-100的桶中。这个过程从头到尾遍历一遍数据只需要500W次。然后根据桶号大小依次将桶中数值输出,即可以得到一个有序的序列。而且可以很容易的得到100分有***人,501分有***人。

      实际上,桶排序对数据的条件有特殊要求,如果上面的分数不是从100-900,而是从0-2亿,那么分配2亿个桶显然是不可能的。所以桶排序有其局限性,适合元素值集合并不大的情况。

例题2

在一个文件中有 10G 个整数,乱序排列,要求找出中位数。内存限制为 2G。只写出思路即可(内存限制为 2G的意思就是,可以使用2G的空间来运行程序,而不考虑这台机器上的其他软件的占用内存)。

    分析: 既然要找中位数,很简单就是排序的想法。那么基于字节的桶排序是一个可行的方法。

    思想:将整型的每1byte作为一个关键字,也就是说一个整形可以拆成4keys,而且最高位的keys越大,整数越大。如果高位keys相同,则比较次高位的keys。整个比较过程类似于字符串的字典序。按以下步骤实施:

    1把10G整数每2G读入一次内存,然后一次遍历这536,870,912即(1024*1024*1024*2 /4个数据。每个数据用位运算">>"取出最高8(31-24)。这8bits(0-255)最多表示255个桶,那么可以根据8bit的值来确定丢入第几个桶。最后把每个桶写入一个磁盘文件中,同时在内存中统计每个桶内数据的数量,自然这个数量只需要255个整形空间即可。
    2继续以内存中的整数的次高8bit进行桶排序(23-16)。过程和第一步相同,也是255个桶。
    3一直下去,直到最低字节(7-0bit)的桶排序结束。我相信这个时候完全可以在内存中使用一次快排就可以了


(九).计数排序算法(实际上就是桶排序算法)

1)算法简介

    计数排序(Counting sort)是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置只能对整数进行排序

2)算法描述和分析

        算法的步骤如下:

          1、找出待排序的数组中最大和最小的元素

         2、统计数组中每个值为i的元素出现的次数,存入数组C的第i项

         3、对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)

         4、反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

        当输入的元素是n 0k之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。

        由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。

3)算法图解、flash演示、视频演示

图解:

        我们使用计数排序对一个乱序的整数数组进行排序。

        首先创建一个临时数组(长度为输入数据的最大间隔),对于每一个输入数组的整数k,我们在临时数组的第k位置"1"如下图


        上图中,第一行表示输入数据,第二行表示创建的临时数据,临时数组的下标代表输入数据的某一个值,临时数组的值表示输入数据中某一个值的数量。

         如果输入数据中有重复的数值,那么我们增加临时数组相应的值(比如上图中53个,所以小标为5的数组的值是3)。在“初始化”临时数组以后,我们就得到了一个排序好的输入数据。

        我们顺序遍历这个数组,将下标解释成数据, 将该位置的值表示该数据的重复数量,记得得到一个排序好的数组。

4)算法代码


【特点】 
1. 提前必须是已知待排序的关键字为整型且范围已知。 
2. 时间复杂度为O(n+k),n指的是桶的个数,k指的是待排序数组的长度,不是基于比较的排序算法,因此效率非常之高。 
3. 稳定性好,这个是计数排序非常重要的特性,可以用在后面介绍的基数排序中。 
4. 但需要一些辅助数组,如C[0..k],因此待排序的关键字范围0~k不宜过大。

import java.util.*;

public class CountingSort {
    public int[] countingSort(int[] A, int n) {
        if(A==null ||n<2){
            return A;
        }
        //找出桶的范围,即通过要排序的数组的最大最小值来确定桶范围
        int min=A[0];
        int max=A[0];
        for(int i=0;i<n;i++){
            min=Math.min(A[i],min);
            max=Math.max(A[i],max);
        }
        //确定桶数组,桶的下标即为需排序数组的值,桶的值为序排序数同一组值出现的次数
        int[] arr = new int[max-min+1];
        //往桶里分配元素
        for(int i=0;i<n;i++){
            arr[A[i]-min]++;
        }

        //从桶中取出元素
        int index=0;
        for(int i=0;i<arr.length;i++){
            while(arr[i]-->0){
                A[index++]=i+min;
            }
        }

        return A;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

(十).基数排序算法(基于桶排序)

1)算法简介

        基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机(Tabulation Machine)上的贡献。

2)算法描述和分析

        整个算法过程描述如下: 

                1、将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。

                2、从最低位开始,依次进行一次排序。

                3、这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

      基数排序的时间复杂度是 O(kn),其中n是排序元素个数,k是数字位数。

      注意这不是说这个时间复杂度一定优于O(n·log(n)),因为k的大小一般会受到n的影响。 以排序n个不同整数来举例,假定这些整数以B为底,这样每位数都有B个不同的数字,k就一定不小于logB(n)。由于有B个不同的数字,所以就需要B个不同的桶,在每一轮比较的时候都需要平均n·log2(B) 次比较来把整数放到合适的桶中去,所以就有:

      k 大于或等于 logB(n)

      每一轮(平均)需要 n·log2(B) 次比较

所以,基数排序的平均时间T就是:

      T ≥ logB(n)·n·log2(B) = log2(n)·logB(2)·n·log2(B) = log2(n)·n·logB(2)·log2(B) = n·log2(n)

      所以和比较排序相似,基数排序需要的比较次数:T ≥ n·log2(n)。 故其时间复杂度为 Ω(n·log2(n)) = Ω(n·log n) 

3)算法图解

图解:

    

4)算法代码


【原理】基数排序(Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

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

【题目】对于一个int数组,请编写一个基数排序算法,对数组元素排序。 
给定一个int数组A及数组的大小n,请返回排序后的数组。保证元素均小于等于2000。

import java.util.*;
import java.lang.Math;
public class RadixSort {
    public int[] radixSort(int[] A, int n) {
        //基于桶排序的基数排序

        //确定排序的趟数,即排序数组中最大值为809时,趟数为3
         int max=A[0];
        for(int i=0;i<n;i++){
            if(A[i]>max){
                max= A[i];
            }
        }
        //算出max的位数
        int time=0;
        while(max>0){
            max/=10;
            time++;
        }
        //【桶】初始化十个链表作为桶,用户分配时暂存
        ArrayList<ArrayList<Integer>> list = new ArrayList<ArrayList<Integer>>();
        for(int i=0;i<10;i++){
            ArrayList<Integer> Item = new ArrayList<Integer>();
            list.add(Item);
        }

        //进行time次分配和收集
        for(int i=0;i<time;i++){

            //分配元素,按照次序优先,从个位数开始
            for(int j=0;j<n;j++){
                int index = A[j]%(int)Math.pow(10,i+1)/(int)Math.pow(10,i);
                list.get(index).add(A[j]);
            }
            //收集元素,一个一个桶地收集
            int count=0;
            //10个桶
            for(int k=0;k<10;k++){
                //每个桶收集
                if(list.get(k).size()>0){

                    for(int a: list.get(k)){
                        A[count]=a;
                        count++;   
                    }
                  //清除数据,以便下次收集
                    list.get(k).clear();
                }
            }
        }
        return A; 
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 【基数排序效率】
  • 基数排序的时间复杂度是O(k·n),其中n是排序元素个数,k是数字位数。注意这不是说这个时间复杂度一定优于O(n·log(n)),k的大小取决于数字位的选择和待排序数据所属数据类型的全集的大小;k决定了进行多少轮处理,而n是每轮处理的操作数目。

  • 基数排序基本操作的代价较小,k一般不大于logn,所以基数排序一般要快过基于比较的排序,比如快速排序。

  • 最差空间复杂度是O(k·n)

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值