【数据结构】排序算法

1,概念

各排序复杂度

1)插入排序

直接插入排序、折半插入排序、2-路插入排序、表插入排序、希尔排序、快速排序、冒泡排序

2)选择排序

简单选择排序、堆排序

2, 直接插入排序(Insertion Sort)

1)原理

每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子序列中的适当位置,直到全部记录插入完成为止。

2)代码实现

public static void insertSort(int[] a){
        for(int i=1; i<a.length; i++){
            int temp = a[i];//待插入元素
            int j;
            for (j=i-1; j>=0; j--){//将大于temp的往后移动一位
                if (temp < a[j]){
                    a[j+1] = a[j];
                }else{
                    break;
                }
            }
            a[j+1] = temp;//插入进来
        }
    }

3)稳定性

稳定。

4)复杂度

①空间复杂度:O(1)。
②时间复杂度:
最好的情况:原来本身有序,比较次数为n-1次时间复杂度为O(n)
最坏的情况:原来为逆序,比较次数为1 + 2 + 3 + … + (N - 1)次 (O(n^2)),而记录的移动次数为1 + 2 + 3 + … + (N - 1)次 (O(n^2)),时间复杂度为 O(n^2)。

5)举例

直接插入排序

3, 折半插入排序

在折半查找的基础上进行的插入排序。

1)原理

①每次插入,都从前面的有序子表中查找出待插入元素应该被插入的位置; (折半查找)
②给插入位置腾出空间,将待插入元素复制到表中的插入位置。

2)代码实现

public static void insertSortBinary(int[] a){
        int low, high, mid;
        int temp;//待插入的数
        for (int i=1; i<a.length; i++){
            temp = a[i];
            //折半查找范围
            low = 0;
            high = i-1;
            while (low <= high){
                mid = (low+high) / 2;
                if (a[mid] > temp){
                    high = mid-1;//查找左半元素
                }else{
                    low = mid+1;
                }
            }
            for(int j=i-1; j>= high + 1; j--){
                a[j+1] = a[j];//后移空出插入位置
            }
            a[high + 1] = temp;//插入
        }
        OutputMessage.showMessage(a);
    }

3)稳定性

稳定的。

4)复杂度

①时间复杂度 :O(n²)
折半插入排序仅仅是减少了比较元素的次数,约为O(nlogn),而且该比较次数与待排序表的初始状态无关,仅取决于表中的元素个数n;而元素的移动次数没有改变,它依赖于待排序表的初始状态。因此,折半插入排序的时间复杂度仍然为O(n²),但它的效果还是比直接插入排序要好。
②空间复杂度:O(1)。
稳定性
折半插入排序是一种稳定的排序算法。

4, 2-路插入排序

1)原理

折半插入排序的改进,通过增加n个记录的辅助空间,减少排序过程中移动记录的次数。
①开辟一个等长的临时数组(循环数组),将待排序数组的第1个元素放到临时数组的第0位,作为初始化。将该值作为每次排序的参照,大于等于这个参照值就后插,小于参照值就前插。
②同时定义两个游标first和final分别指向临时数组当前最小值和最大值所在位置。

2)代码实现

       public static void TwoInsertSort(int[] a){
        int first=0, finals = 0;
        int length = a.length;
        int []b = new int[length];//辅助空间
        b[0] = a[0];
        for(int i = 1; i < length; i++){
            if(a[i] > b[finals]){
                finals++;
                b[finals] = a[i];
            }
            if(a[i] < b[first]){
                first=(first-1+length)%length;
                b[first]=a[i];
            }
            if(a[i] > b[first] && a[i]< b[finals]){//用循环数组理解
                int j = finals;
                finals++;
                while(a[i] <= b[j]){
                    b[(j+1)%length] = b[j];
                    j = (j-1+length)%length;
                }
                b[(j+1)%length] = a[i];////若j停留在数组最后的位置(j+1)%length
            }
            OutputMessage.showMessage(b);
            System.out.print("finals=" + finals + ",first=" + first);
        }
        /*
         * 将数组重新复制到a[]数组中(调整循环数组顺序)
         */
        for(int i = 0; i < length;i++){
            a[i] = b[(first++)%length];
        }
        OutputMessage.showMessage(a);
    }

3)稳定性

稳定

4)复杂度

和折半复杂度一致,但避免了数据的移动(不是绝对的避免)。

5)举例

2路插入

5, 表插入排序

1)原理

将一个记录插入到已排好序的有序链表中,通过修改指针的值来代替移动记录。
利用静态链表的形式,分两部完成:
①对一个有序的循环链表,插入一新的元素,修改每个节点的后继指针的指向,使顺着这个指针的指向,元素是有序的。在这个过程中,我们不移动或交换元素,只是修改指针的指向。
②顺着指针的指向调整元素的位置,使其在链表中真正做到物理有序。

2)代码实现

  /**
     * 表插入排序,相当于静态链表
     * 从小到大排序
     */
    public static void table_insertion_sort(int[] a) {

        TableSortEntity[] arr = getTableSortArr(a);
        int current, pre;//current当前节点 pre前一个节点
        for (int i = 2; i < a.length + 1; i++) { //修改next域,使其按指针指向有序
            pre = 0;
            current = arr[pre].getNext();
            while (arr[i].getData() >= arr[current].getData()) {     // >= 保证了排序的稳定性
                pre = current;
                current = arr[pre].getNext();
            }
            arr[i].setNext(current);
            arr[pre].setNext(i);
        }
        current = arr[0].getNext();
        int i = 0;
        while (current != 0) {  //顺着静态链表的指针指向,回写数据到原数组

            a[i++] = arr[current].getData();
            current = arr[current].getNext();
        }
        array_table(arr);
    }
private static TableSortEntity[] getTableSortArr(int[] a) {
        int MAX = 20;
        TableSortEntity[] arr = new TableSortEntity[a.length + 1];//arr[0]为头结点,为循环终止创造条件,头结点值域MAX应大于原序列中的最大值。
        for (int i = 0; i < a.length; i++) {  //把数据赋给链表
            TableSortEntity tableSortEntity = new TableSortEntity();
            tableSortEntity.setData(a[i]);
            tableSortEntity.setNext(0);
            arr[i + 1] = tableSortEntity;
        }
        TableSortEntity tableSortEntity = new TableSortEntity();
        tableSortEntity.setData(MAX);
        tableSortEntity.setNext(1);//头节点和第一个节点构成了循环链表
        arr[0] = tableSortEntity;
        return arr;
    }
private static void array_table(TableSortEntity[] arr) {
        int q, p = arr[0].getNext();
        for (int i = 1; i < arr.length; i++) {
            while (p < i)
                p = arr[p].getNext();
            q = arr[p].getNext();    // q记录下一次待归位节点的下标
            if (p != i) {  //如果p与i相等,则表明已在正确的位置上,那就不需要调整了
                TableSortEntity temp = arr[i];
                arr[i] = arr[p];
                arr[p] = temp;
                arr[i].setNext(p);
            }
            p = q;
        }
        System.out.println("排序结果:");
        for (int j = 1; j < arr.length; j++) {

            System.out.print(arr[j].getData() + ",");
        }
    }
public class TableSortEntity {
    int data;   //值域
    int next;   //静态链表的链域
}

3)稳定性

稳定

4)复杂度

时间复杂度:O(n2),避免了移动。

5)举例

表插入排序

6, 分段插入排序

1)原理

①已知一组升序排列数据a[1]、a[2]、……a[n],一组无序数据b[1]、b[2]、……b[m],需将二者合并成一个升序数列。先将数组a分成x等份(x<<n),每等份有n/x个数据。将每一段的第一个数据先储存在数组c中:c[1]、c[2]、……c[x]。
②运用插入排序处理数组b中的数据。插入时b先与c比较,确定了b在a中的哪一段之后,再到a中相应的段中插入b。随着数据的插入,a中每一段的长度会有变化,所以在每次插入后,都要检测一下每段数据的量的标准差s,当其大于某一值时,将a重新分段。在数据量特别巨大时,可在a中的每一段中分子段,b先和主段的首数据比较,再和子段的首数据比较,可提高速度。

2)优缺点

优点:快,比较次数少;
缺点:不适用于较少数据的排序,s的临界值无法确切获知,只能凭经验取。

7, 希尔排序(Shell’s Sort,缩小增量排序,Diminishing Increment Sort)

1)原理

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

2)代码实现

  /**
     * 希尔排序 针对有序序列在插入时采用交换法
     * @param arr
     */
    public static void shellSort(int[] arr) {
        //增量gap,并逐步缩小增量
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
            //从第gap个元素,逐个对其所在组进行直接插入排序操作
            for (int i = gap; i < arr.length; i++) {
                int j = i;
                while (j - gap >= 0 && arr[j] < arr[j - gap]) {
                    //插入排序采用交换法
                    swap(arr, j, j - gap);
                    j -= gap;
                }
            }
        }
    }

3)稳定性

不稳定。

4)复杂度

最坏时间复杂度依然为O(n2),一些经过优化的增量序列如Hibbard经过复杂证明可使得最坏时间复杂度为O(n3/2).
Shell排序比冒泡排序快5倍,比插入排序大致快2倍。Shell排序比起QuickSort,MergeSort,HeapSort慢很多。但是它相对比较简单,它适合于数据量在5000以下并且速度并不是特别重要的场合。它对于数据量较小的数列重复排序是非常好的。

5)举例

如下图所示。
首先,步长k=5,则R1和R6分为一组,依次类推,共5个子序列:{R1,R6},{R2,R7},{R3,R8},{R4,R9},{R5,R10}.
在每一对子序列内,从小到大排序,然后合并结果为第一趟排序结果。

第二趟排序,k=4。以此类推,直到k=1停止。
希尔排序

8, 快速排序

1)原理

整体思想:
①先从数列中取出一个数作为基准数。
②分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
③再对左右区间重复第二步,直到各区间只有一个数。
例子

一次划分:
①设置2个变量i=0、j=N-1;
②第一个数为基准key;
③j向前搜索,j–,找到第一个比key小的数A[j],将A[i]与A[j]交换;
④i向后搜索,i++,找到第一个比key大的数A[i],将A[i]与A[j]交换;
⑤重复第三、四步,直到i>=j。
一次划分

2)代码实现

package QuickSort;
/**
 * 快速排序(从大到小)
 * @author luo
 * 分析:
 * 1,分解:以a[p]为基准将啊[p:r]划分为3段:a[p:q-1](小于基准元素),a[q](等于基准元素即就是a[p]),a[q+1:r](大于基准)
 * 2,递归求解
 * 3,合并
 */
public class QuickSort {

    /**
     * 递归算法
     * @param a
     * @param p 起始下标
     * @param r 结束下标
     */
    public static void QuickSort1(int[] a,int p,int r){

        if (p < r){
            int q = Partition(a,p,r);
            QuickSort1(a,p,q-1);
            QuickSort1(a,q+1,r);
        }
    }
    /**
     * 划分元素
     * a[p]为基准元素
     * @param a
     * @param p 划分起始下标
     * @param r 划分结束下标
     * @return
     */
    public static int Partition(int a[],int p,int r){

        int temp=0;//存储交换
        int i=p,j=r+1;
        /**基准*/
        int x=a[p];

        while(true)
        {
            while(a[++i]>x && i<r);//找出比x大的数
            while(a[--j]<x);//找出比x小的数
            if(i >= j)
            {
                break;
            }
            temp=a[i];
            a[i]=a[j];
            a[j]=temp;
        }
        a[p]=a[j];
        a[j]=x;
        return j;
    }
}
package QuickSort;

public class Main{

    public static void main(String[] args){
        int[] a= {2,3,8,9,1};
        QuickSort.QuickSort1(a, 0, 4);
        for (int i = 0; i < a.length; i++) {
            System.out.println(a[i]);
        }
    }
}

3)稳定性

不稳定。(在快速排序的随机选择比较子阶段)

4)复杂度

快速排序的运行时间和划分是否对称有关。

①最坏情况

划分的2个区域包含n-1个和1个元素。
T(n) = T(n-1) + O(n) = O(n^2)

②最好情况

划分对称,2个区域都为n/2个元素。
T(n) = 2T(n/2) + O(n) = O(nlogn)

9, 冒泡排序(Bubble Sort,泡沫排序或气泡排序)

1)原理

它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

2)代码实现

改进(增加交换标记)

package AllSort;

import java.nio.channels.ShutdownChannelGroupException;

public class Main {

    public static void main(String[] args){

        int[] arr = new int[]{1,9,4,2,8};

        BubbleSort.sort(arr);
        show("冒泡排序结果:", arr);       
    }

    private static void show(String s, int[] arr){

        String result = s;
        for (int i = 0; i < arr.length; i++) {
            result += arr[i];
            result += ",";
        }
        System.out.println(result);
    }
}
package AllSort;

public class Swap {

    public static void Swap(int[] arr, int a, int b){
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
}
package AllSort;
/**
 * 冒泡排序
 * @author luo
 *
 */
public class BubbleSort {

    public static void sort(int[] a){
        boolean isSwap = false;//交换标准。未发生交换为false
        for (int i = 0; i < a.length; i++) {

            isSwap = false;
            for(int j = 0; j< a.length - 1 - i; j++){
                if (a[j] > a[j+1]) {
                    Swap.Swap(a, j, j+1);
                    isSwap = true;
                }
            }
            if(!isSwap){
                break;
            }
        }
    }
}

3)稳定性

稳定。

4)复杂度

最好的情况,n-1次比较,移动次数为0,时间复杂度为O(n)。
最坏的情况,n(n-1)/2次比较,等数量级的移动,时间复杂度为O(O^2)。
平均时间复杂度:O(n^2)

空间复杂度:O(1)

10, 简单选择排序

1)原理

设所排序序列的记录个数为n。i取1,2,…,n-1,从所有n-i+1个记录(Ri,Ri+1,…,Rn)中找出排序码最小的记录,与第i个记录交换。执行n-1趟 后就完成了记录序列的排序。

2)代码实现

    /**
     * 简单选择排序
     * 原理:从i到args.length-1,每次迭代将i到args.length-1中最小(最大)的那个数交换到i的位置,然后i++,再循环
     * @param array 待排序的数组
     */
    public static void simpleSelectMethod(int[] array) {
        //minLoc用于记录i+1到args.length-1这个区间的最小值的下标(i会递增),i表示要交换的位置。
        for (int i = 0, j = 0, minLoc = 0; i < array.length; i++) {
            minLoc = i;
            for (j = i + 1; j < array.length; j++) {//找出i+1到args.length-1这个区间的最小值的下标
                if (array[j] < array[minLoc]) {
                    minLoc = j;
                }
            }

            if (minLoc != i) {//如果minLoc!=i,说明minLoc有变化,就进行交换
                int temp = array[i];
                array[i] = array[minLoc];
                array[minLoc] = temp;
            }
        }
        OutputMessage.showMessage(array);
    }

3)稳定性

不稳定。

4)复杂度

最坏情况:比较次数为 n(n-1)/2,交换次数为n-1;
最好情况:比较次数为 n(n-1)/2,交换次数为0。

平均:O(n^2)

11,堆排序

1)原理

①构建最大堆。
②选择顶,并与第0位置元素交换
③由于步骤2的的交换可能破环了最大堆的性质,第0不再是最大元素,需要调用maxHeap调整堆(沉降法),如果需要重复步骤2

是对树型选择排序占用空间多的改进。

堆的定义
定义一:
小(大)根堆:完全二叉树中任意结点小于(大于)其孩子。
定义二:
n个元素的序列{k1,k2,…,kn}满足以下关系时,称为堆。
堆

2)代码实现

 public static void heapSort(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }

        buildMaxHeap(array);

        for (int i = array.length - 1; i >= 1; i--) {
            ArrayUtils.exchangeElements(array, 0, i);

            maxHeap(array, i, 0);
        }
        OutputMessage.showMessage(array);
    }

    private static void buildMaxHeap(int[] array) {
        if (array == null || array.length <= 1) {
            return;
        }

        int half = array.length / 2;
        for (int i = half; i >= 0; i--) {
            maxHeap(array, array.length, i);
        }
    }

    private static void maxHeap(int[] array, int heapSize, int index) {
        int left = index * 2 + 1;
        int right = index * 2 + 2;

        int largest = index;
        if (left < heapSize && array[left] > array[index]) {
            largest = left;
        }

        if (right < heapSize && array[right] > array[largest]) {
            largest = right;
        }

        if (index != largest) {
            ArrayUtils.exchangeElements(array, index, largest);

            maxHeap(array, heapSize, largest);
        }
    }
package util;

public class ArrayUtils {
    public static void printArray(int[] array) {
        System.out.print("{");
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i]);
            if (i < array.length - 1) {
                System.out.print(", ");
            }
        }
        System.out.println("}");
    }

    public static void exchangeElements(int[] array, int index1, int index2) {
        int temp = array[index1];
        array[index1] = array[index2];
        array[index2] = temp;
    }
}

3)稳定性

不稳定

4)复杂度

时间复杂度 O(nlogn)

5)举例

输出对顶元素后,调整剩余元素成为一个新堆
调整堆

将无序序列建成一个堆:
对于无序序列{49,38,65,97,76,13,27,49},依次放入二叉树。自叶子结点到堆顶筛选,如图所示。
新建堆

12, 合并排序(Merging Sort,归并排序)

1)原理

将待排序元素分成大小大致相同的2个子集,分别对2个子集(继续分)进行排序,最终将排好序的子集合并。

2)代码实现

递归、分治法

package MergeSort;

/**
 * 合并排序
 * @author luo
 *
 */
public class MergeSort {

    /**
     * 递归法
     * @param a
     * @param n
     */
    public static void MergeSort1(int[] a, int left, int right){

        int b[] = new int[right + 1];//用来存储的
        if(left < right){//下标合法(至少有2个元素)
            int mid = (left + right)/2;//取中点
            MergeSort1(a, left, mid);//排序前半部分
            MergeSort1(a, mid + 1, right);//排序后半部分
            Merge(a, b, left, mid, right);//将a的前后两半段合并排序
            Copy(a, b, left, right);//b复制回a
        }
    }
    /**
     * 将b数组复制回a
     * @param a
     * @param b
     * @param left 起始下标
     * @param right 结束下标
     */
    public static void Copy(int[] a, int[] b, int left, int right){
        for (int i = left; i <= right; i++) {
            a[i] = b[i];
        }
    }
    /**
     * 
     * 分治思想的合并排序
     * 基本思想:将待排序元素分成大小大致相同的2个子集合,分别对2个集合排序,最终将排序好的子集合合并。
     * @param a
     * @param n
     */
    public static void MergeSort2(int[] a, int n){
        int b[] = new int[n];
        /**合并大小*/
        int size = 1;

        while(size < n){
            MergePass(a, b, size, n);//合并到b数组
            size += size;
            MergePass(b, a, size, n);//合并到a数组
        }
    }
    /**
     * 合并到y数组,合并大小为s的相邻子数组
     * @param x 
     * @param y x和y合并到y数组
     * @param s 合并大小
     * @param n
     */
    public static void MergePass(int[] x, int[] y, int s, int n){

        int i=0,j;//循环变量

        while(i <= n-2*s){
            Merge(x,y,i,i+s-1,i+2*s-1);
            i = i + 2 * s;
        }
        //剩下的元素个数少于2s
        if( i+s < n )
            Merge( x, y, i, i+s-1, n-1 );
        else
            for( j=i; j<=n-1; j++ )
                y[j] = x[j];
    }
    /**
     * 合并操作:合并排好序的函数,放在d数组
     * 合并 c[l:m]和 c[l+m: r] 到 d[l:r],既将2个子集合合并为一个大的集合
     * @param c
     * @param d
     * @param l
     * @param m
     * @param r
     */
    public static void Merge(int[] c, int[] d, int l, int m, int r){

         int i=l, j=m+1, index_d = l, q;

         //判断下标有没有越界
            while( (i <= m) && (j <= r) ){

                if(c[i]<=c[j]){

                    //一般比较第i个数和第i+1的数
                    d[index_d++]=c[i++];//前者小,将前者放到d数组中
                }else{

                    d[index_d++]=c[j++];//否则将后者放到d数组中
                }
            }
            if(i>m){
                for(q=j; q<=r; q++){
                    d[index_d++] = c[q];
                }
            }else{
                for(q=i;q<=m;q++){
                    d[index_d++]=c[q];
                }
            }
    }
}
package MergeSort;

import java.util.Scanner;

public class Main{

    private static int MAX_SIZE = 10;

    public static void main(String[] args){

        int i,n;//n是个数
        int a1[] = new int[MAX_SIZE];//升序排列,head[]里放小数,tail放大数。
        int a2[] = new int[MAX_SIZE];//升序排列,head[]里放小数,tail放大数。

        System.out.println("请输入您需要排序数的总数:   ");
        Scanner in = new Scanner(System.in);
        n = in.nextInt();

        System.out.println("请输入排序数: "); 
        for(i=0; i<n; i++){
            a1[i] = in.nextInt();
            a2[i] = in.nextInt();
        }
        MergeSort.MergeSort1( a1, 0, n-1 );//合并排序
        MergeSort.MergeSort2( a2, n );//合并排序

        System.out.println("排序完成:");

        String result_Str1 = "";
        String result_Str2 = "";
        for(i=0; i<n; i++){
            result_Str1 += a1[i];
            result_Str1 += "  ";
            result_Str2 += a2[i];
            result_Str2 += "  ";
        }
        System.out.println("MergeSort1:" + result_Str1);
        System.out.println("MergeSort2:" + result_Str2);
    }
}

3)稳定性

稳定。

4)复杂度

T(n)=2T(n/2) + O(n) = O(nlogn)

5)举例

13, 基数排序(Radix Sorting,桶排序bucket sort)

基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort。 

1)原理

将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。

①最高位优先(MSD,Most Significant Digit first)法

先按k1排序分组,同一组中记录,关键码k1相等,再对各组按k2排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码kd对各子组排序后。再将各组连接起来,便得到一个有序序列。

②最低位优先(LSD,Least Significant Digit first)法

先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列。

2)代码实现

public static void RadixSort(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++;
        }
    }

3)稳定性

稳定。

4)复杂度

时间效率 :设待排序列为n个记录,d个关键码,关键码的取值范围为radix,则进行链式基数排序的时间复杂度为O(d(n+radix)),其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(radix),共进行d趟分配和收集。
空间效率:需要2*radix个指向队列的辅助空间,以及用于静态链表的n个指针。

5)举例

以LSD为例,假设原来有一串数值如下所示:
73, 22, 93, 43, 55, 14, 28, 65, 39, 81
首先根据个位数的数值,在走访数值时将它们分配至编号0到9的桶子中:
0
1 81
2 22
3 73 93 43
4 14
5 55 65
6
7
8 28
9 39
第二步
接下来将这些桶子中的数值重新串接起来,成为以下的数列:
81, 22, 73, 93, 43, 14, 55, 65, 28, 39
接着再进行一次分配,这次是根据十位数来分配:
0
1 14
2 22 28
3 39
4 43
5 55
6 65
7 73
8 81
9 93
第三步
接下来将这些桶子中的数值重新串接起来,成为以下的数列:
14, 22, 28, 39, 43, 55, 65, 73, 81, 93
这时候整个数列已经排序完毕;如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。

LSD的基数排序适用于位数小的数列,如果位数多的话,使用MSD的效率会比较好。MSD的方式与LSD相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,将每个桶子中的数值按照下一数位的值分配到“子桶”中。在进行完最低位数的分配后再合并回单一的数组中。

14,Bit排序算法(位排序、bitmap算法)

1)原理

32位机器上,一个整形,比如int a; 在内存中占32bit位,可以用对应的32bit位对应十进制的0-31个数,bitmap算法利用这种思想处理大量数据的排序与查询.

①表示

第一个4就是(从最后一位开始,分别代表0 1 2 3 4 ……) 
00000000000000000000000000010000 
而输入2的时候 
00000000000000000000000000010100 
输入3时候 
00000000000000000000000000011100 
输入1的时候 
00000000000000000000000000011110

思想比较简单,关键是十进制和二进制bit位需要一个map图,把十进制的数映射到bit位。

②map映射表

假设需要排序或者查找的总数N=10000000,那么我们需要申请内存空间的大小为int a[1 + N/32],其中:a[0]在内存中占32位可以对应十进制数0-31,依次类推:
bitmap表为:

a[0]--------->0-31 
a[1]--------->32-63 
a[2]--------->64-95 
a[3]--------->96-127 
……

那么十进制数如何转换为对应的bit位,下面介绍用位移将十进制数转换为对应的bit位。

③位移转换

对于十进制数n, n/32为对应数组下标;n%32为数组内第几个数。
如:n=24,那么 n/32=0,则24在a[0]数组中,左右往左第n%32=24位的位置上。

利用移位0-31使得对应32bit位为1.

2)优缺点

优点:
1.运算效率高,不许进行比较和移位;
2.占用内存少,比如N=10000000;只需占用内存为N/8=1250000Byte=1.25M。
缺点:
所有的数据不能重复。即不可对重复的数据进行排序和查找。

3)举例

对1亿个不重复的正整数(0-99999999)进行排序,内存只有50MB。(出自《编程珠玑》)
思路:充分利用计算机的每一个bit位来节约空间。如上需求中,我们预设1亿个bit位,初始置0。从待排序集合中每读取一个数,在对应的第多少位bit上置1。最后,遍历该bit位集合,每个为1的bit位,则输出对应的正整数,最终输出结果已经排序完成,所需内存空间仅需:1亿bit/8=12.5MB。
实现方案:使用int数组来模拟bit位序列。1 int = 4 byte = 32 bit,因此需要1亿/32=3125000个int数来表达1亿个bit位。

4)C代码实现

#include <stdio.h>

#define BITSPERWORD 32
#define SHIFT 5
#define MASK 0x1F
#define N 10000000

int a[1 + N/BITSPERWORD];//申请内存的大小

//set 设置所在的bit位为1
//clr 初始化所有的bit位为0
//test 测试所在的bit为是否为1

void set(int i) {        a[i>>SHIFT] |=  (1<<(i & MASK)); }
void clr(int i) {        a[i>>SHIFT] &= ~(1<<(i & MASK)); }
int  test(int i){ return a[i>>SHIFT] &   (1<<(i & MASK)); }

int main()
{   int i;
    for (i = 0; i < N; i++)
        clr(i);
    while (scanf("%d", &i) != EOF)
        set(i);
    for (i = 0; i < N; i++)
        if (test(i))
            printf("%d\n", i);

    return 0;
}

windows系统下,在输入回车换行后的空行位置,按 ctrl+z,再回车确认,即可输入EOF。
代码中:
①i>>SHIFT
i右移5位,2^5=32,相当于i/32,即求出十进制i对应在数组a中的下标。
②i & MASK
其中MASK=0X1F,十六进制转化为十进制为31,二进制为0001 1111,i&(0001 1111)相当于保留i的后5位。 相当于i%32

15, TopN算法

有N(N>>10000)个整数,求出其中的前K个最大的数。(TopK问题,Top10问题)。
1.最简单的方法:将n个数排序,排序后的前k个数就是最大的k个数,这种算法的复杂度是O(nlogn)(选出的这m个数排好序了)
2.O(n)的方法:利用快排的patition思想,基于数组的第m个数来调整,将比第m个数小的都位于数组的左边,比第m个数大的都调整到数组的右边,这样调整后,位于数组右边的m个数最大的数(这m个数不一定是排好序的)(选出的这m个数可能无序) 如下方法(3)
3.O(nlogk)的方法:先创建一个大小为m的最小堆,接下来我们每次从输入的n个整数中读入一个数,如果这个数比最小堆的堆顶元素还要大,那么替换这个最小堆的堆顶并调整。如下方法(1)(2)

1)采用小顶堆或者大顶堆

①原理

根据数据前K个建立K个节点的小顶堆,在后面的N-K的数据的扫描中,如果数据大于小顶堆的根节点,则根节点的值覆为该数据,并调节节点至小顶堆。如果数据小于或等于小顶堆的根节点,小根堆无变化。

②场景

求最大K个采用小顶堆,而求最小K个采用大顶堆。

③代码

package sort;

import io.OutputMessage;

public class TopKByHeap {
    /**
     * 创建k个节点的小根堆
     *
     * @param a
     * @param k
     * @return
     */
   private static int[] createHeap(int a[], int k) {
        int[] result = new int[k];
        for (int i = 0; i < k; i++) {
            result[i] = a[i];
        }
        for (int i = 1; i < k; i++) {
            int child = i;
            int parent = (i - 1) / 2;
            int temp = a[i];
            while (parent >= 0 &&child!=0&& result[parent] >temp) {
                result[child] = result[parent];
                child = parent;
                parent = (parent - 1) / 2;
            }
            result[child] = temp;
        }
        return result;

    }

    private static void insert(int a[], int value) {
        a[0]=value;
        int parent=0;

        while(parent<a.length){
            int lchild=2*parent+1;
            int rchild=2*parent+2;
            int minIndex=parent;
            if(lchild<a.length&&a[parent]>a[lchild]){
                minIndex=lchild;
            }
            if(rchild<a.length&&a[minIndex]>a[rchild]){
                minIndex=rchild;
            }
            if(minIndex==parent){
                break;
            }else{
                int temp=a[parent];
                a[parent]=a[minIndex];
                a[minIndex]=temp;
                parent=minIndex;
            }
        }
    }

    public static int[] getTopKByHeap(int input[], int k) {
        int heap[] = createHeap(input, k);
        for(int i=k;i<input.length;i++){
            if(input[i]>heap[0]){
                insert(heap, input[i]);
            }
        }
        return heap;
    }
}
 TopKByHeap.getTopKByHeap(a,3);

2)PriorityQueue优先队列:

①原理

PriorityQueue是从JDK1.5开始提供的新的数据结构接口,它是一种基于优先级堆的极大优先级队列。优先级队列是不同于先进先出队列的另一种队列。每次从队列中取出的是具有最高优先权的元素。如果不提供Comparator的话,优先队列中元素默认按自然顺序排列,也就是数字默认是小的在队列头,字符串则按字典序排列(参阅 Comparable),也可以根据 Comparator 来指定,这取决于使用哪种构造方法。优先级队列不允许 null 元素。依靠自然排序的优先级队列还不允许插入不可比较的对象(这样做可能导致 ClassCastException)。

PriorityQueue构造固定容量的优先队列,模拟大顶堆,这种队列本身数组实现,无容量限制,可以指定队列长度和比较方式,然后将数据依次压入,当队列满时会poll出小值,最后需要注意的是,priorityQueue本身遍历是无序的,可以使用内置poll()方法,每次从队首取出元素。

②代码

package sort;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Random;
/**
 * 固定容量的优先队列,模拟大顶堆,用于解决求topN小的问题
 * @author 张恩备
 * @date 2016-11-25 下午02:29:31
 */
public class FixSizedPriorityQueue<E extends Comparable> {
    private PriorityQueue<E> queue;
    private int maxSize; // 堆的最大容量

    public FixSizedPriorityQueue(int maxSize) {
        if (maxSize <= 0)
            throw new IllegalArgumentException();
        this.maxSize = maxSize;
        this.queue = new PriorityQueue(maxSize, new Comparator<E>() {
            public int compare(E o1, E o2) {
                // 生成最大堆使用o2-o1,生成最小堆使用o1-o2, 并修改 e.compareTo(peek) 比较规则
                return (o2.compareTo(o1));
            }
        });
    }

    public void add(E e) {
        if (queue.size() < maxSize) { // 未达到最大容量,直接添加
            queue.add(e);
        } else { // 队列已满
            E peek = queue.peek();
            if (e.compareTo(peek) < 0) { // 将新元素与当前堆顶元素比较,保留较小的元素
                queue.poll();
                queue.add(e);
            }
        }
    }


    public List<E> sortedList() {
        List<E> list = new ArrayList<E>(queue);
        Collections.sort(list); // PriorityQueue本身的遍历是无序的,最终需要对队列中的元素进行排序
        return list;
    }

    public void printResult(){
        // 或者直接用内置的 poll() 方法,每次取队首元素(堆顶的最大值)
        while (!queue.isEmpty()) {
            System.out.print(queue.poll() + ", ");
        }
    }
}
 final FixSizedPriorityQueue pq = new FixSizedPriorityQueue<Integer>(3);

        for (int i=0; i<a.length; i++) {
            pq.add(a[i]);
        }
        pq.printResult();

3)快排过程法

①原理

实现步骤:根据快排规则,选择一个数为基准(代码是以最后一个数)分化数据,并记录基准数最后的落点下标,最后判断下标和k-1值大小(下标从0开始),不相等就继续朝k-1数量方向分化:
下标小于k-1,对下标右侧(partion,end)继续二分;
下标大于k-1,对下标左侧(first,partion)继续二分;
直到k个数为top,但这k个数并没有顺序。

②场景

适用于无序单个数组,快排过程法利用快速排序的过程来求Top k。

③代码

public class TopK{  
    int partion(int a[],int first,int end){  
          int i=first;  
          int main=a[end];  
          for(int j=first;j<end;j++){  
                 if(a[j]<main){  
                     int temp=a[j];  
                     a[j]=a[i];  
                     a[i]=temp;  
                     i++;  
                 }  
          }  
          a[end]=a[i];  
          a[i]=main;  
          return i;    
    }  
    void getTopKMinBySort(int a[],int first,int end,int k){  
        if(first<end){  
             int partionIndex=partion(a,first,end);  
             if(partionIndex==k-1)return;  
             else if(partionIndex>k-1)getTopKMinBySort(a,first,partionIndex-1,k);  
             else getTopKMinBySort(a,partionIndex+1,end,k);  
        }  
    }  
public static void main(String []args){  
        int a[]={2,20,3,7,9,1,17,18,0,4};  
        int k=6;  
        new TopK().getTopKMinBySort(a,0,a.length-1,k);  
        for(int i=0;i<k;i++){  
            System.out.print(a[i]+" ");  
        }  
    }  
}  

4)合并法

①原理

实现描述:采用Merge的方法,设定一个数组下标扫描位置记录临时数组和top结果数组,然后从临时数组记录下标开始遍历所有数组并比较大小,将最大值存入结果数组,最大值对应所在数组下标加一存入临时数组,以使其从下位开始遍历,时间复杂度为O(k*m)。(m:为数组的个数)。

②场景

这种方法适用于几个数组有序的情况,来求Top k。

③代码

/** 
 * 已知几个递减有序的m个数组,求这几个数据前k大的数 
 * a[4,3,2,1],b[6,5,3,1] -> result[6,5,4] 
 */  
public class TopKByMerge{  
 public static int[] getTopK(List<List<Integer>>input,int k){  
    int index[]=new int[input.size()];//保存每个数组下标扫描的位置;  
    int result[]=new int[k];  
    for(int i=0;i<k;i++){  
       int max=Integer.MIN_VALUE;  
       int maxIndex=0;  
       for(int j=0;j<input.size();j++){  
           if(index[j]<input.get(j).size()){  
                if(max<input.get(j).get(index[j])){  
                    max=input.get(j).get(index[j]);  
                    maxIndex=j;  
                }  
           }  
       }  
       if(max==Integer.MIN_VALUE){  
           return result;  
       }  
       result[i]=max;  
       index[maxIndex]+=1;  

    }  
    return result;  
 }   
 public static void main(String[] args) {  
     List<Integer> a = new ArrayList<Integer>();  
     a.add(4);  
     a.add(3);  
     a.add(2);  
     a.add(1);  
     List<Integer> b = new ArrayList<Integer>();  
     b.add(6);  
     b.add(5);  
     b.add(3);  
     b.add(1);  
     List<List<Integer>> ab = new ArrayList<List<Integer>>();  
     ab.add(a);  
     ab.add(b);  
     int r[] = getTopK(ab, 3);  
     for (int i = 0; i < r.length; i++) {  
        System.out.println(r[i]);  
    }  
}  
} 

16,其它

猜你喜欢

转载自blog.csdn.net/sunshinetan/article/details/79629602