排序算法

上一篇博客中写到了关于动态规划中一些常见的问题和解决方案,根据问题总结出来解决动态规划类问题的方法是通过寻找状态,列出状态转移方程,通过遍历即可将问题解决。排序也是一类常见的问题,通过排序的思想,我们也可以来解决很多问题,还有贪心,分治等思想,也会在接下来中不断的更新。排序算法,这里要讲的有7种。

  • 插入排序

  • 选择排序

  • 希尔排序

  • 快速排序

  • 三向切分快速排序

  • 堆排序

  • 归并排序

  • 桶排序

接下来从其实现思路和其复杂度上进行分析来讲解这7种算法。

选择排序

这是排序中最简单的一种方式,当前的位置的数和后续位置的进行比较,小的转移到前面,然后不断的进行比较,由于为了减少每次比较后,位置的交换所带来的消耗,通常会通过一个标志位来进行标记,然后,当一次比较结束后进行位置上的交换。这也就是所谓的冒泡排序,通过排序小的数字不断的向上移动,直到到达最上面。

实现代码

public void sort(int[] array) {
    if(array == null || array.length <= 1)
        return;
    int len = array.length;
    for (int i = 0; i < len - 1; i++) {
        for (int j = i + 1; j < len; j ++) {
            if (array[j] < array[i])
                exch(array, i, j);
        }
    }
}

public void exch(int []array,int a,int b) {
    array[a] = array[a] + array[b];
    array[b] = array[a] - array[b];
    array[a] = array[a] - array[b];
}

时间复杂度: O(n^2) |空间复杂度: O(1) |不稳定

插入排序

选择排序中,如果对于一个给定的已经排好序的数组,我们的复杂度仍然是和一个乱序的数组的复杂度相同,而插入排序则可以解决这个问题,插入排序的思想是在遍历的时候如果当前的数小于前面的数,则将其向前移动直到其合适的位置,这样如果是一个已经排序好的数组,其复杂度为n,乱序则为n^2.

实现代码

public void sort(int[] array) {
    if (array == null || array.length <=1)
        return;
    for (int i = 1; i < array.length; i++) {
        for (int j = i; (j > 0) && (array[j] < array[j-1]); j--) {
            exch(array, j-1, j);
        } 
    }
}

此处在第二层循环的判断语句,将判断语句放置在for的判断语句中,使得代码变得简洁了很多,如果不这样的话,需要在内部循环外套一层判断,然后循环内部再做一个终止跳转。

时间复杂度:O(n~n^2) |空间复杂度:O(1)| 稳定

希尔排序

希尔排序是在插入排序上进行的改进,在插入排序中,如果一个较小的数字在后面的话,移动起来会很浪费时间,希尔排序的思路是通过设置不为1的间距,将一些较小的数字移动的前面,从而减少移动的次数。比较抽象。

实现代码

public void sort(int[] array) {
    if (array == null || array.length <= 1)
        return;
    int h = 1;
    while (h < array.length/3) h = h * 3 + 1;
    while (h >= 1) {
        for (int i = h; i < array.length; i++) {
            for (int j = i; j >= h && array[j] < array[j-h]; j-=h)
                exch(array, j, j-h);
        }
        h = h/3;
    }
}

复杂度要比插入排序在某些情况下略好,具体不好计算。
h为我们设置的移动距离,开始为h的计算方法,然后按照插入排序的思路解决。只是移动的距离又原来的1变为现在的不等距离。最终还是回归到1上。

归并排序

归并排序,提到归并,就会有两种方式,一种是从低向上,一种是从上向下,从上向下为递归的方式,而从下向上为迭代的方式来解决。自顶向下,是首先将数组分成两个部分,然后递归这两个部分进行归并排序,最后再将这两部分进行合并。自底向上则是从最小的合并单元开始,不断的增加合并的步长,最后步长和我们的数组的长度相同了,则

实现代码


private int[] aux;
public void sort (int[] array) {
    if (array == null || array.length <= 1)
        return;
    aux = new int[array.length];
    sort(a, 0, array.length - 1);
}

public void sort(int[] array, int low, int high) {
    if (low >= high)
        return;
    int mid = low + (high - low)/2;
    sort(array, low, mid);
    sort(array, mid+1, high);
    merge(array, low, mid, high);
}

public void sort(int[] array, int low, int high) {
    if (low >= high)
        return;
    for (int size = 1; size < N ; size += size) {
        for (int low = 0; low <= high; low += 2*size)
            merge(array, low, low+size-1, Math.min(low  + 2*size -1, high -1));
    }
}

public void merge (int[] array, int low, int mid, int high) {
    int i = low, j = mid+1;
    for (int k = low; k < high; k++) 
        aux[k] = array[k];
    int k = low;
    while(k < high) {
        if (i > mid) {
            array[k++] = aux[j++];
            continue;
        }
        if (j > high) {
            array[k++] = aux[i++];
            continue;
        }
        if(aux[i] > aux[j]) {
            array[k++] = aux[j++];
        }else {
            array[k++] = aux[i++];
        }
    }
}

时间复杂度:O(nlgn)|空间复杂度:O(n)|稳定

借助一个中间数组来实现合并操作。

堆排序

堆排序的实现方式是通过建立一个大堆或者是小堆,也就是所谓的优先队列,然后通过优先队列来实现数值的出队列来实现排序。这里通过维护一个最大堆,然后通过这个堆来进行排序,对于堆的操作,有下沉和上浮,首先,我们通过一个数组来表示这个结构,为了方便进行上浮和下沉,我们将数组的起始位置标记为1,方便我们在向上升和向下沉的操作。

实现代码

private int []array;
private int N = 0;

public MaxPQ(int maxN) {
    array = new int[maxN+1];
}

public boolean isEmpty() {
    return N == 0;
}

public int delMax() {
    int max = -1;
    if (N <= 0)
        return;
    max = array[1];
    array[1] = array[N];
    array[N] = 0;
    N--;
    sink(1);
    return array[1];
}

public void insert(int num) {
    array[++N] = num;
    swim(N);
}

public void swim(int n){
    while(n > 1 && array[n] > array[n/2]){
        exch(n, n/2);
        n = n/2;
    }
}

public void sink(int n) {
    while(2*n < N) {
        if(array[n] >= array[2*n] && array[n] >= array[2*n+1])
            break;
        int tmp = array[2*n] > array[2*n+1] ? 2*n : 2*n+1;
        exch(n, tmp);
        n = tmp;
    }
} 

public void exch(int i, int j) {
    int tmp = array[i];
    array[i] = array[j];
    array[j] = tmp;
}

public void sort() {
    if(N <= 0)
        return;
    for (int k = N/2; k > 0; k--){
        sink(k);
    }
    while(N > 1){
        exch(1, N--);
        sink(1);
    }
}

时间复杂度:O(nlgn)|空间复杂度:N|不稳定

快速排序

快速排序采用的方式通过切分的方式,每次切分,将数组划分为两部分,一部分大于切分点,一部分小于切分点。然后通过不断地切分来实现排序。
实现代码

public void sort(int[] array, int low, int high) {
    if (low >= high)
        return;
    int i = partition(array, low, high);
    sort(array, low, i-1);
    sort(array, j+1, high);
}

private int partition(int[] array, int low, int high) {
    int lowFlag = low;
    int highFlag = high+1;
    while(true){
        while(array[++lowFlag] <= array[low]) if(lowFlag == high) break;
        while(array[--highFlag] >= array[low]) if(highFlag == low) break;
        if(lowFlag >= highFlag) break;
        exch(array, lowFlag, highFlag);
    }
    exch(array, low, highFlag);
    return highFlag;
}

private void exch(int[] array, int i, int j){
    int tmp = array[i];
    array[i] = array[j];
    array[j] = tmp;
}

时间复杂度:O(nlgn)|空间复杂度:O(1)|不稳定

核心代码在于切分部分,如何将我们的数组切分为两个部分,得到了第一个位置,之后,将其和数组的前部开始部分和后部进行比较,比较完成之后,前面为小于它的数字,后面为大于它的数字,如果不符合进行交换,最后返回切分位置。

三向切分快速排序

三向切分快速排序是在其快速排序的基础上来解决对于数组中的重复数字的问题,如果按照第一种方式,及时是相同的数字,也要被重新切分一次,因此想到通过这种方式,对于相同的数字只会被切分一次,因此需要增加一个标志位来控制相同的部分。
实现代码

public void sort(int[] array, int low, int high){
    if(high <= low)
        return;
    int lowFlag = low;
    int highFlag = high;
    int i = low+1;
    while(i < highFlag){
        int tmp = array[low];
        if(array[i] < tmp) exch(array, i++, lowFlag++);
        else if(array[i] > tmp) exch(array, i, highFlag--);
        else i++;
    }
    sort(array, low, lowFlag-1);
    sort(array, highFlag+1, high);
}

如果遇到比第一位,也就是要作为切分点的数小的数,则将其与之进行交换,同时记录下来其位置,如果遇到比它大的数,则和后面的数进行交换,最后就可以将相同的数集中在一起。在上面的快速排序中,对于每一次判断没有进行一个交换,而是等到了最后的时候,才进行一个交换。

时间复杂度:O(nlgn)|空间复杂度:O(1)|不稳定

Java中的sort方法采用的是三向切分快速排序,但是该种算法有一个不好的地方是,在对于一个倒序的数组,这个时候,复杂度会比较高,因此通常会将其进行乱序操作,将数组打乱,然后再进行排序。

桶排序

桶排序相比上述方法来说是非常简单的,其复杂度是在n,但是空间开销是非常大的,特比是当数据集分布的不是很集中的时候,将会浪费很大的空间,属于典型的用空间来换取时间的一种做法。

实现代码

public void bucketSort(int[] array) {
    if(array == null)
        return;
    int[] bucket = new int[11];
    for (int i = 0; i < array.length; i++) {
        bucket[array[i]]++;
    }
    int k = 0;
    for(int j = 0; j < bucket.length; j++) {
        while(bucket[j]-- > 0) {
            array[k++] = j;
        }
    }
}

这里的桶做的是只能容纳


Jensen95
2.9k 声望534 粉丝

连续创业者。