排序

when ? why ? how ? what?

how

怎么衡量一个排序算法好坏?

稳定性、时间复杂度、空间复杂度。

what

什么叫稳定性?

如有有两个数 A,B 它们的值相等且 A 在 B 之前,如果经过某个排序算法排序后 A,B 的相对位置没有改变(A 在 B 之前)说明这个排序算法是稳定的,如果它们的相对位置发生了改变说明这个排序算法是不稳定的。

什么叫内部排序?什么叫外部排序?

内部排序:待排序记录存放在内存中进行的排序过程。

外部排序:待排序记录的数量很大,以致于内存不能一次容纳全部记录,所以在排序过程中需要对外存进行访问的排序过程。

什么叫简单排序?

简单插入排序、简单选择排序、冒泡排序。

选择排序

为什么简单选择排序和堆排序属于选择排序?

以前自己都没有好好思考,因为这两个排序都选择出最值来交换。

简单选择排序

思路

如果有一堆数,你需要将其排序(升序)。 遍历选出最小的元素与第一个元素交换,再从第二元素个开始遍历选出后面中的最小的元素与第二个元素交换,重复直到最后...

public void simpleSelectionSort(int[] data){
    if(data.length<2 || data==null){
        return;
    }
    for(int i=0;i<data.length;i++){
        int min=i;
        for(int j=i+1;j<data.length;j++){
            if(data[j]<data[min]){
                min=j;
            }
        }
        //判断最小值有没有换,从而进行交换
        if(i!=min){
            swap(data,i,min);
        }
    }
}

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

无论在什么情况下都需要比较 N*(N-1)/2 次,所以时间复杂度是 O(N^2)。不稳定

堆排序

之前有介绍过

最大堆

因为最大堆的堆顶元素是最大的值,将它删除并将其保存在一个额外空间的辅助数组中,然后将最后一个元素放在堆顶然后进行调整,重复上诉操作直至将堆中元素都删除,然后将辅助数组赋值给原来的数组。 时间复杂度 O(NlogN),空间复杂度 O(N)。

还有一种更聪明的方法,空间复杂度为 O(1),就是删除最大元素时将最大元素和堆最后一个元素值交换后调整。

public void heapSort(Heap heap,int[] data){
    //元素个数
    int num=data.length;
	//建造最大堆
    heap.buildMaxHeap(heap,data);
	//最大值交换调整
    for(int i=num-1;i>0;i--){
        swap(heap,i,0);
        heap.size--;
        precDown(heap,0);
    }

    //遍历查看
    for(int i=0;i<num;i++){
        System.out.print(heap.data[i]+"  ");
    }
}
//交换
public void swap(Heap heap,int i,int j){
    int temp=heap.data[i];
    heap.data[i]=heap.data[j];
    heap.data[j]=temp;
}
//下滤(核心) 
public void precDown(Heap heap, int p) {
    int temp = heap.data[p];
    int parent=p, child;
    for ( ;parent*2+1< heap.size; parent = child) {
        child = 2 * parent + 1;
        if (child != heap.size - 1 && heap.data[child] < heap.data[child + 1]) {
            //比较左右孩子结点大小
            child++;
        }
        if (temp < heap.data[child]) {
            heap.data[parent] = heap.data[child];
        }else{
            break;
        }
    }
    heap.data[parent] = temp;
}

public void buildMaxHeap(Heap heap, int[] data) {
    if (isFull(heap)) {
        System.out.println("Heap is full");
        return;
    }
    if((capacity-size-1)<data.length){
        System.out.println("容量不足");
        return;
    }
    for (int i = 0; i < data.length; i++) {
        heap.data[heap.size++] = data[i];
    }
    for (int i = data.length/ 2 - 1 ; i >= 0; i--) {
        precDown(heap, i);
    }
}

堆排序时间复杂度 O(NlogN),空间复杂度可以是 O(1) 第二种,O(N)第一种。 不稳定。

插入排序

打扑克和插入排序很像,当你每摸一张扑克的时候需要插入到原先已经有序的序列并调整。

简单插入排序

public void insertionSort(int[] data) {
    if (data.length < 2 || data == null) {
        return;
    }
    for (int i = 0; i < data.length; i++) {
        int temp = data[i];
        int j;
        for (j = i; j > 0 && data[j - 1] > temp; j--) {
            data[j] = data[j - 1];
        }
        data[j] = temp;
    }
}

平均时间复杂度 O(N^2),最坏 O(N^2),空间复杂度 O(1),稳定

希尔排序

简单插入排序的效率不高一个重要原因是每次只交换相邻的两个元素,这样只能消去一对错位元素。希尔排序是对插入排序进行改进,试图通过每次交换相隔一定距离的元素,达到排序效率上的提升。

public void shellSort(int[] data, int increment) {
    int d, p, i;
    for (d = increment; d > 0; d = d / 2) {
        for (p = d; p < data.length; p++) {
            int temp = data[p];
            for (i = p; i >= d && data[i - 1] > temp; i = i - d) {
                data[i] = data[i - d];
            }
            data[i] = temp;
        }
    }
}

最坏情况 Θ(N^2),空间复杂度 O(1),不稳定

当shell排序增量选的不好时就有问题效率就没简单插入排序高。

下面两个增量可以改善效率

交换排序

冒泡排序

比较相邻的元素,一次循环交换后出现一个最大值(或最小值)位置固定。

public void bubbleSort(int[] data) {
    //标记
    boolean flag;
    for (int i = 0; i < data.length; i++) {
        flag = false;
        //循环一次最大元素被换到最右边
        for (int j = 1; j < data.length - i; j++) {
            if (data[j - 1] > data[j]) {
                //标记有交换过
                flag = true;
                swap(data, j - 1, j);
            }
        }
        if (flag == false) {
            break;
        }
    }
}

时间复杂度 O(N^2),空间复杂度 O(1),稳定

快速排序

快速排序:将未排序的元素根据一个作为基准的“主元”(piovt)分为两个子序列,其中一个子序列的记录均大于主元,而另一个子序列均小于主元,然后递归地对这两个子序列用类似的方法进行排序。采用分治法,将问题规模减小,然后再分别进行处理。

什么是快速度排序算法的最好情况?

每次正好中分———— O(NlogN) 。

选主元:取头、中、尾的中位数。

在子集划分中如果有元素等于pivot怎么办?

  1. 停下来交换它们
  2. 不理它,继续移动

取一个极端的例子当所有元素的值都相等时,停下来交换,那么就会往中间靠,将规模划分为两个等长的子集这样的时间复杂度是 O(NlogN)。

如果不理它,继续移动虽然省去了交换的操作,但是这样每次主元都会被放在端点上如上面的囧这样时间复杂度是 O(NlogN)。 所以选择停下来交换它们比较好。

快速排序在现实中应用比较好。但是小规模还不如插入排序快,所以在递归的时候检查子问题的规模,当其小于某个阀值时就不继续递归,而直接使用简单插入排序解决问题。

public int median3(int[] data, int left, int right) {
    int center = (left + right) / 2;
    //交换排列出 data[left] <= data[center] <= data[right]
    if (data[left] > data[center]) {
        swap(data, left, center);
    }
    if (data[left] > data[right]) {
        swap(data, left, right);
    }
    if (data[center] > data[right]) {
        swap(data, center, right);
    }
    swap(data,center,right-1);
    //只需要考虑 data[left+1],data[right-2]
    return data[right-1];
}

public void qSort(int[] data,int left,int right){
    //cutoff 判断数组规模 如果规模大用快排,规模小用简单插入排序,规模自己定义
    int pivot,cutoff=1000,low,high;
    if(cutoff <= right- left){
        pivot=median3(data,left,right);
        low=left;
        high=right;
        //将序列中比基准小的移到基准左边,大的移到右边
        while(true){
            while(data[++low]<pivot){
            }
            while(data[--high]>pivot){
            }
            if(low<high){
                swap(data,low,high);
            }else{
                break;
            }
        }
        //将基准换到正确的位
        swap(data,low,right-1);
        //递归
        qSort(data,left,low-1);
        qSort(data,low+1,right);
    }else{
        //元素太少,用简单排序
        insertionSort(data);
    }
}

public void quickSort(int[] data){
    qSort(data,0,data.length-1);
}

平均时间复杂度 O(NlogN),最坏的时间复杂度 O(N^2), 空间复杂度 O(1) ,不稳定

归并排序

核心是有序子列的归并

/**
 * @param data
 * @param tmpData
 * @param left     左边起始位置
 * @param right    右边起始位置
 * @param rightEnd 右边终点位置
 */
public void merge(int[] data, int[] tmpData, int left, int right, int rightEnd) {
    //左边终点位置
    int leftEnd = right - 1;
    //存放结果的数组的初始位置
    int temp = left;
    int num = rightEnd - left + 1;
    while (left <= leftEnd && right <= rightEnd) {
        if (data[left] < data[right]) {
            tmpData[temp++] = data[left++];
        } else {
            tmpData[temp++] = data[right++];
        }
    }
    //直接复制左边
    while (left <= leftEnd) {
        tmpData[temp++] = data[left++];
    }
    //直接复制右边
    while (right <= rightEnd) {
        tmpData[temp++] = data[right++];
    }
    for (int i = 0; i < num; i++) {
        data[rightEnd - i] = tmpData[rightEnd - i];
    }
}

递归算法

public void mSort(int[] data, int[] tmpData, int left, int rightEnd) {
    int center;
    if (left < rightEnd) {
        center = (left + rightEnd) / 2;
        mSort(data, tmpData, left, center);
        mSort(data, tmpData, center + 1, rightEnd);
        merge(data, tmpData, left, center + 1, rightEnd);
    }
}

public void mergeSort(int[] data) {
    if (data.length < 2 || data == null) {
        return;
    }
    int[] tempData = new int[data.length];
    mSort(data, tempData, 0, data.length - 1);
}

如果mSort和merge方法不传入辅助数组 tmpData,而是在函数中重新创建
那么如下图,显然是我们不想要的。

非递归算法

 /**
 * @param data
 * @param tmpData
 * @param left     左边起始位置
 * @param right    右边起始位置
 * @param rightEnd 右边终点位置
 */
public void mergeTwo(int[] data, int[] tmpData, int left, int right, int rightEnd) {
    //左边终点位置
    int leftEnd = right - 1;
    //存放结果的数组的初始位置
    int temp = left;
    int num = rightEnd - left + 1;
    while (left <= leftEnd && right <= rightEnd) {
        if (data[left] < data[right]) {
            tmpData[temp++] = data[left++];
        } else {
            tmpData[temp++] = data[right++];
        }
    }
    //直接复制左边
    while (left <= leftEnd) {
        tmpData[temp++] = data[left++];
    }
    //直接复制右边
    while (right <= rightEnd) {
        tmpData[temp++] = data[right++];
    }
}

/**
 * @param data
 * @param tmpData
 * @param length  当前有序子序列的长度
 */
public void mergePass(int[] data, int[] tmpData, int length) {
    int i, j;
    for (i = 0; i <= data.length - 2 * length; i += 2 * length) {
        mergeTwo(data, tmpData, i, i + length , i + 2 * length - 1);
    }
    //归并最后两个子列
    if (i + length < data.length) {
        mergeTwo(data, tmpData, i, i + length, data.length - 1);
    } else {
        for (j = i; j < data.length; j++) {
            tmpData[j] = data[j];
        }
    }
}

/**
 * 归并非递归
 *
 * @param data
 */
public void mergeSortNonRecursive(int[] data) {
    if (data.length < 2 || data == null) {
        return;
    }
    int length = 1;
    int[] tmpData = new int[data.length];
    while (length < data.length) {
        mergePass(data, tmpData, length);
        length *= 2;
        mergePass(tmpData, data, length);
        length *= 2;
    }
}

归并排序时间复杂度 O(NlogN), 空间复杂度 O(N), 稳定

基数排序

桶排序

什么叫桶排序?

如果已知 N 个关键字的取值范围是在 0 到 M-1 之间,而 M 比 N 小得多,则桶排序算法将为关键字的每个可能取值建立一个“桶”,即建立 M 个桶,在扫描 N 个关键字时,将每个关键字放入相应的桶中,然后按桶的顺序收集一遍就自然有序了。所以桶排序效率比一般的排序算法高————当然需要额外条件是已知道关键字的范围,并且关键字在此范围内是可列的,个数还不能超过内存空间所能承受的限度。

假设有 N 个学生,他们的成绩 0 到 100 之间的整数(于是有 M= 101 个不同的成绩值 )。如何在线性时间内将学生按成绩排序

当 M 远远大于 N 时怎么办?

基数排序。

基数排序

基数排序是桶排序的一种推广,它所考虑的待排记录包含不止一个关键字。

假设有 N=10 个整数,每个整数的值在 0 到 999 之间。

B 是桶数量,P 是分配收集的趟数。

多关键字的排序

一副扑克牌是按 2 种关键字排序的

不过扑克牌排序 LSD 比 MSD 快。

次位优先

class Node {
    int key;
    Node next;
}

class HeadNode {
    Node head, tail;
}

 public int getDigit(int x, int d, int radix) {
    //默认次为d=1
    int num = -1, i;
    for (i = 1; i <= d; i++) {
        num = x % radix;
        x /= radix;
    }
    return num;
}

public void lsdRadixSort(int[] data, int maxDigit, int radix) {
    //次位优先
    HeadNode[] bucket = new HeadNode[radix];
    int i, num, d;
    Node tmp = null, p = null, list = null;
    //初始化每个桶为空链表
    for (i = 0; i < radix; i++) {
        bucket[i]=new HeadNode();
    }
    //将原始序列逆序存入初始链表List
    for (i = 0; i < data.length; i++) {
        tmp = new Node();
        tmp.key = data[i];
        tmp.next = list;
        list = tmp;
    }
    //对数据的每一位循环处理
    for (d = 1; d <= maxDigit; d++) {
        p = list;
        while (p!=null) {
            //获取当前元素的当前位数字
            num = getDigit(p.key, d, radix);
            //从 list 中摘除
            tmp = p;
            p = p.next;
            tmp.next = null;
            if (bucket[num].head == null) {
                bucket[num].head = tmp;
                bucket[num].tail = tmp;
            } else {
                bucket[num].tail.next = tmp;
                bucket[num].tail = tmp;
            }
        }
        //收集
        list=null;
        for(num=radix-1;num>=0;num--){
            if(bucket[num].head!=null){
                //整桶插入list表头
                bucket[num].tail.next=list;
                list=bucket[num].head;
                //清空桶
                bucket[num].head=bucket[num].tail=null;
            }
        }
    }
    for(i=0;i<data.length;i++){
        data[i]=list.key;
        list=list.next;
    }
}

时间复杂度 O(D(N+R)),空间复杂度 O(N+R),稳定 R 为桶数,D 为分配收集的趟数。


逆序对

对于下标 i < j,如果 A[i] > A[j],则称(i,j)是一对逆序对(inversion)。

具体代码

总结

参考

数据结构 陈越 何钦铭

基数排序:适合处理数据量大、关键字取值范围有限的序列。时间复杂度 O(P(N+B)),空间复杂度 O(N+B),稳定。

除了基数排序外其它排序都是建立在交换和比较操作上的,决定其性能主要是比较、交换的次数和是否需要额外空间用于保存临时值。

简单排序:元素规模 N 较小或基本有序,使用简单排序比较好(简单插入排序、冒泡排序、简单选择排序)。这 3 个排序的时间复杂度都一样 O
(N^2),空间复杂度 O(1),冒泡和简单插入排序是稳定的,简单选择排序是不稳定的。

希尔排序:排序增量序列取值和排序对象的具体情况有关,最差情况时间复杂度接近直接插入排序 O(N^2)。 增量序列可使用 Hibbard 增量序列或 Sedgewick 增量序列。 空间复杂度 O(1),不稳定。

时间效率表现较好的是快速排序、堆排序和归并排序,都采用分而治之的方法。 堆排序在堆顶元素输出后需要寻找下一个堆顶元素,在寻找过程中不断将问题规模减少。快输排序在寻找基准后,序列划分外两个部分,两个部分内部各自进行比较,两个部分之间没有进行比较。归并排序将规模减半再进行排序,在规模为 N 时再进行复杂度为 O(N)的归并操作。 这三种排序时间复杂度 O(NlogN),但是到具体实践的平均时间效率上,快速排序无疑是最佳的排序方法。在最坏情况下,快速排序时间效率不如堆和归并排序其时间复杂度是 O(N^2),快速排序需要 O(logN)深度的栈空间。

快速排序最优的情况下空间复杂度为:O(logn) ;每一次都平分数组的情况,最差的情况下空间复杂度为:O( n );退化为冒泡排序的情况。

归并排序需要 O(N)的辅助空间,堆排序需要常数个额外空间 O(1)。

归并排序稳定,快速排序和堆排序不稳定。

每一种排序都有其自身优点,适用于不同情况。

有什么问题欢迎指出,十分感谢!

posted @ 2018-07-18 22:18  罗贱人  阅读(203)  评论(0编辑  收藏  举报