數據結構與算法(七),排序

轉載請註明出處:http://www.cnblogs.com/wangyingli/p/5994256.htmlhtml

這節總結一下常見的排序算法。java

說明:因爲對對象元素進行排序須要實現Comparable接口,這裏爲了實現簡單,方便測試,僅對整數進行排序(即排序的對象爲整型數組)。算法

一、插入排序

排序思想:把待排序的元素按其值的大小逐個插入到一個已經排好序的序列中,直到全部的元素插入完爲止。數組

排序過程:數據結構

1.一、直接插入排序

其代碼以下:dom

//核心代碼
public void sort(int[] data) {
    int size = data.length;
    for (int i = 1; i < size; i++) {
        for (int j = i; j >= 1 && data[j] < data[j-1]; j--) {
             swap(data, j, j-1);
        }
    }
}

//交換兩個元素
public void swap(int[] data,int i,int j) {
    int temp = data[i];
    data[i] = data[j];
    data[j] = temp;
}

在內循環中將較大的元素一次性向右移動而不是交換兩個元素,這樣訪問數組的次數將減半 。其代碼以下:性能

public void sort(int[] data) {
    int size = data.length;
    for (int i = 1; i < size; i++) {
        int temp = data[i];
        int index = 0; //要插入的位置
        for (int j = i; j >= 1; j--) {
            if (temp < data[j-1]) {
                data[j] = data[j-1];
            }else {
                index = j;
                break;
            }
        }
        data[index] = temp;
    }
}

1.二、二分插入排序

將直接插入排序中尋找a[i]插入位置的方法改成二分查找,而後再一次性向右移動元素。測試

public void sort(int[] data) {
    int size = data.length;
    for (int i = 1; i < size; i++) {
        int num = binaryFind(data, data[i], 0, i-1);
        int temp = data[i];
        //num後的元素向後移動
        for (int j = i; j > num; j--) {
           data[j] = data[j-1];
        }
        data[num] = temp;
    }
}

//找出元素應在數組中插入的位置
public int binaryFind(int[] data, int temp, int down, int up) {
    if(up<down || up>data.length || down<0) {
        System.out.println("下標錯誤");
    }
    if(temp < data[down]) return down;
    if(temp > data[up]) return up+1;
    int mid = (up-down)/2 + down;
    if(temp == data[mid]) {
        return mid + 1;
    }else if(temp < data[mid]) {
        up = mid-1;
    }else if(temp > data[mid]) {
        down = mid+1;
    }
    return binaryFind(data,temp, down, up);
}

二分插入排序減小了比較次數,特別是當要排序的數據很大時,這個效果將更加明顯。3d

二、選擇排序

排序思想:每一次從待排序的數據元素中找出最小(或最大)的一個元素,將它和待排序的元素中第一個位置的元素進行交換,直到所有待排序的數據元素排完程爲止。指針

排序過程:

代碼:

public void sort(int[] data) {
    int size = data.length;
    for(int i=0;i<size;i++) {
        int min = i;
        for(int j=i+1;j<size;j++) {
            if(data[j] < data[min]) 
                min=j;
        }
        swap(data,i,min);
    }
}

//交換兩個元素
public void swap(int[] data,int i,int j) {
    int temp = data[i];
    data[i] = data[j];
    data[j] = temp;
}

三、冒泡排序

排序思想:依次比較相鄰的兩個元素,若它們的順序錯誤則交換,每次循環都將最大(或最小)元素放在序列一端。

排序過程:

代碼:

public void sort(int[] data) {
    int size = data.length;
    for(int i=0; i<size; i++) {
        for(int j=0; j<size-i-1; j++) {
            if(data[j] > data[j+1]) 
                swap(data,j,j+1);
        }
        
    }
}

//交換兩個元素
public void swap(int[] data,int i,int j) {
    int temp = data[i];
    data[i] = data[j];
    data[j] = temp;
}

冒泡排序與選擇排序的區別:

  • 冒泡排序法是兩兩依次比較,並作交換,交換的次數多。
  • 選擇排序法是每次循環找出最值,循環結束後將最值調整到合適位置,交換的次數少。

四、歸併排序

排序過程:

4.一、自頂向下的歸併排序

排序思想:要將一個數組排序,能夠先(遞歸的)將它分爲兩半分別排序,而後將結果歸併起來。

private int[] array; //輔助數組
public void sort(int[] data) {
    array = new int[data.length];
    mergeSort(data, 0, data.length - 1);

}

//核心算法
public void mergeSort(int[] data, int down, int up) {
    if (up <= down) return; //結束條件
    int mid = (up - down) / 2 + down;
    mergeSort(data, down, mid); //左半邊排序
    mergeSort(data, mid + 1, up); //右半邊排序
    merge(data, down, mid, up);
}

//一個數組左右半邊分別有序,歸併
public void merge(int[] data, int down, int mid, int up) {
    int i = down, j = mid + 1;
    //複製數組中元素
    for (int k = down; k <= up; k++) {
        array[k] = data[k];
    }

    for (int k = down; k <= up; k++) {
        if (i > mid) 
            data[k] = array[j++]; //左半邊用盡,取右半邊元素
        else if (j > up) 
            data[k] = array[i++];
        else if (array[i] < array[j]) //左半邊元素比右半邊小
            data[k] = array[i++];
        else 
            data[k] = array[j++];
    }
}

對於自頂向下的歸併排序的改進:

  • 因爲歸併排序的方法調用過於頻繁,會產生過多的額外開銷,因此歸併排序在處理小規模問題時,比插入排序要慢。在用歸併排序處理大規模數據時,使用插入排序來處理小規模的子數組,通常可以使歸併排序的運行時間縮短10%-15%。

  • 添加一個判斷,若是data[mid]小於data[mid+1],則數組已經有序,不須要進行歸併操做。這樣可大大減少有序子數組的運行時間。

  • 經過在遞歸調用的每一個層次交換輸入數組和輔助數組的角色,節省元素複製到輔助數組中的時間(空間不行)。即在遞歸中,數據從輸入數組排序到輔助數組和從輔助數組排序到輸入數組交替使用。

/*
 * 改進自頂向下的歸併排序
 * 1. 對小規模數組使用插入排序
 * 2. 加入數組是否有序的判斷,減小歸併次數
 * 3. 經過在遞歸中交換參數,避免數組複製
 */
public class MergeInsSort {
    public static final int CUTOFF = 5; //插入排序處理數組長度
    private int[] array;

    public void sort(int[] a) {
        array = a.clone();
        mergeSort(array, a, 0, a.length - 1);

    }

    //核心算法, 對dst進行排序
    public void mergeSort(int[] src, int[] dst, int down, int up) {
        //改進,小規模用插入排序,結束條件
        if (up - down <= CUTOFF) {
            insertionSort(dst, down, up);
            return;
        }
        int mid = (up - down) / 2 + down;
        mergeSort(dst, src, down, mid); //左半邊排序,交換輸入數組和輔助數組角色
        mergeSort(dst, src, mid + 1, up); //右半邊排序,結果:src中有序

        if(src[mid] < src[mid + 1]) { //是否已經有序
            System.arraycopy(src, down, dst, down, up-down+1);
            return;
        }
        merge(src, dst, down, mid, up);
    }

    //一個數組左右半邊分別有序,src歸併到dst
    public void merge(int[] src, int[] dst, int down, int mid, int up) {
        assert isSorted(src, down, mid);  //斷言,左右半邊均有序
        assert isSorted(src, mid+1,up);

        int i = down, j = mid + 1;
        for (int k = down; k <= up; k++) {
            if (i > mid) dst[k] = src[j++]; //左半邊用盡,取右半邊元素
            else if (j > up) dst[k] = src[i++];
            else if (src[i] < src[j]) //左半邊元素比右半邊小
                dst[k] = src[i++];
            else dst[k] = src[j++];
        }
        assert isSorted(dst, down, up);
    }

    //插入排序
    public void insertionSort(int[] a, int down, int up) {
        for (int i = down+1; i <= up; i++) {
            for (int j = i; j >= down+1 && a[j] < a[j-1]; j--) {
                swap(a, j, j-1);
            }
        }
    }

/*******************************************************************************/

    //交換兩個元素
    public void swap(int[] a,int i,int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
    
    //判斷down到up的元素是否有序
    public boolean isSorted(int[] a, int down, int up) {
        for (int i = down+1; i <= up; i++) {
            if (a[i] < a[i - 1])
                return false;
        }
        return true;
    }
}

4.二、自底向上的歸併排序

排序思想:先兩兩歸併(把每一個元素當成一個長度爲1的數組),在四四歸併,而後八八歸併,一直下去。每輪歸併中最後一次歸併的第二個數組可能比第一個小(注意不要越界)。

private int[] array; //輔助數組

public void sort(int[] a) {
    array = new int[a.length];
    mergeSort(a);
}

//核心算法
public void mergeSort(int[] a) {
    int N = a.length;
    for (int i = 1; i < N; i = 2 * i) {
        for (int j = 0; j < N - i; j += 2 * i)
            merge(a, j, j + i - 1, Math.min(j + 2 * i - 1, N - 1));
    }
}

//一個數組左右半邊分別有序,歸併
public void merge(int[] a, int down, int mid, int up) {
    int i = down, j = mid + 1;
    //複製數組中元素
    for (int k = down; k <= up; k++) {
        array[k] = a[k];
    }

    for (int k = down; k <= up; k++) {
        if (i > mid) a[k] = array[j++]; //左半邊用盡,取右半邊元素
        else if (j > up) a[k] = array[i++];
        else if (array[i] < array[j]) //左半邊元素比右半邊小
            a[k] = array[i++];
        else a[k] = array[j++];
    }
}

五、希爾排序

排序思想:交換不相鄰的元素以對數組的局部進行排序,並最終用插入排序將局部有序的數組排序。使數組中任意間隔爲h的元素都是有序的。其中h爲任意以1結尾的整數序列。

希爾排序的執行時間依賴於增量序列h,好的增量序列的共同特徵:

  • 最後一個增量必須爲1;
  • 應該儘可能避免序列中的值(尤爲是相鄰的值)互爲倍數的狀況。

排序過程:

希爾排序比插入排序要快的多,而且數組越大,優點越大。

代碼:

//核心算法,增量序列 1 4 13 ....(3*h+1)
public void sort(int[] data) {
    int size = data.length;
    int h = 1;
    while(h < size/3) 
        h = 3*h + 1;
    while(h > 0 ) {
        //插入排序,間隔h
        for(int i=h; i<size; i++) {
            for(int j=i; j>=h && data[j] < data[j-h]; j = j-h) {
                swap(data, j, j-h);
            }
        }
        h = h/3;
    }

}

//交換兩個元素
public void swap(int[] data,int i,int j) {
    int temp = data[i];
    data[i] = data[j];
    data[j] = temp;
}

六、快速排序

排序思想:經過一趟排序將要排序的數據切分紅獨立的兩部分,其中一部分的全部數據都比另一部分的全部數據都要小,而後再按此方法對這兩部分數據分別進行快速排序,整個排序過程能夠遞歸進行,以此達到整個數據變成有序序列。

切分過程:

取數組中第一個元素做爲切分元素V,從數組的左端開始向右掃描直達找到一個大於等於V的元素,再從數組右端向左掃描直到找到一個小於等於V的元素,交換他們的位置。如此繼續,保證左指針左側的元素都小於等於V,右指針右側的元素都大於等於V。直到兩個指針相遇,將V和左子數組最右側元素交換,返回j

排序過程:

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

//核心算法
public void sort(int[] data, int down, int up) {
    if(up <= down)
        return;
    
    int temp = partition(data, down, up); //找到切分點
    sort(data, down, temp - 1); //左半邊排序
    sort(data, temp + 1, up);  //右半邊排序
}

//切分
public int partition(int[] data, int down, int up) {
    int i = down;
    int j = up + 1;
    int v = data[down]; //使用data[down]做爲切分元素
    while (true) {
        while (data[++i] < v) {
            if (i == up) 
                break;
        }

        while (v < data[--j]) {
            if (j == down) 
                break;
        }

        if (i >= j) break;
        swap(data, i, j);
    }
    swap(data, down, j);
    return j;
}

//交換兩個元素
public void swap(int[] data, int i, int j) {
    int temp = data[i];
    data[i] = data[j];
    data[j] = temp;
}

快速排序的改進:

快速排序切分不平衡時可能會很是低效,如:第一次以最小的元素切分,第二次以第二小的元素切分,如此,每次調用只會移動一個元素,這將使快速排序退化爲冒泡排序。因此快速排序前要將數組進行隨機排序,打亂其順序。另外對於小規模數組,可使用插入排序來提升排序的性能。緣由和歸併排序時同樣。

改進後的代碼:

//使用插入排序的闕值
public static final int CUTOFF = 5;
public void sort(int[] data) {
    shuffle(data); //打亂數組,消除對輸入的依賴
    sort(data, 0, data.length - 1);
}

public void sort(int[] data, int down, int up) {
    //小規模時用插入排序
    if (up - down <= CUTOFF) {
        insertionSort(data, down, up);
        return;
    }
    
  //大規模時使用快速排序
    int temp = partition(data, down, up); //切分
    sort(data, down, temp - 1); //左半邊排序
    sort(data, temp + 1, up);  //右半邊排序
}

//切分
public int partition(int[] data, int down, int up) {
    int i = down;
    int j = up + 1;
    while (true) {
        //使用data[down]做爲切分元素
        while (data[++i] < data[down]) {
            if (i == up) 
                break;
        }

        while (data[down] < data[--j]) {
            if (j == down) 
                break;
        }

        if (i >= j) break;
        swap(data, i, j);
    }
    swap(data, down, j);
    return j;
}

//插入排序
public void insertionSort(int[] data, int down, int up) {
    for (int i = down + 1; i <= up; i++) {
        for (int j = i; j >= down + 1 && data[j] < data[j - 1]; j--) {
            swap(data, j, j - 1);
        }
    }
}

//交換兩個元素
public void swap(int[] data, int i, int j) {
    int temp = data[i];
    data[i] = data[j];
    data[j] = temp;
}

//隨機打亂數組
public static void shuffle(int[] data) {
    int N = data.length;
    Random rand = new Random();
    for (int i = 0; i < N; i++) {
        int r = i + rand.nextInt(N-i);     // between i and N-1
        int temp = data[i];
        data[i] = data[r];
        data[r] = temp;
    }
}

七、堆排序

排序思想:利用堆的有序性(根節點最大)來進行排序,每次從堆中取出根節點,並保持堆有序。

若是對堆這種數據結構不太瞭解的話,能夠先看個人另外一篇博客《數據結構與算法(五),優先隊列》。

這裏須要注意:這篇博客中實現的堆是從數組中下標爲0的位置開始的,因此結點k的子結點下標分別爲2k+1和2k+2,這和在《數據結構與算法(五),優先隊列》中的堆實現有些不一樣。

排序過程:

代碼:

public void sort(int[] data) {
    int N = data.length-1;
    for(int k = (N-1)/2; k>=0; k--) {
        sink(data, k, N); //使堆有序
    }
    
    //將最大元素放到最後
    while(N > 0) {
        swap(data, 0, N--);
        sink(data, 0, N);
    }
}

//下沉操做,從下標0開始
private void sink(int[] data, int k, int N) {
    while(2*k+1 <= N) {
        int j = 2*k+1;
        if(j < N && data[j] < data[j+1])
            j++;
        if(data[k] >= data[j])
            break;
        swap(data, k, j);
        k = j;
    }
}

//交換兩個元素
public void swap(int[] data, int i, int j) {
    int temp = data[i];
    data[i] = data[j];
    data[j] = temp;
}

八、性能比較

說明:這裏只比較通用的實現方法,而不會對排序方法的改進版本進行比較。

排序的穩定性:

假定在待排序的記錄序列中,存在多個具備相同的關鍵字的記錄,若通過排序,這些記錄的相對次序保持不變,即在原序列中,ri=rj,且ri在rj以前,而在排序後的序列中,ri仍在rj以前,則稱這種排序算法是穩定的;不然稱爲不穩定的。

相關文章
相關標籤/搜索