動畫:一篇文章快速學會快速排序

內容介紹

快速排序簡介

快速排序(Quicksort)是對冒泡排序的一種改進。快速排序由C. A. R. Hoare在1960年提出。快速排序算法被列爲20世紀十大算法之一,這足以說明的他的做用和重要性。快速排序是程序員必須掌握的一種排序算法。java

希爾排序至關於直接插入排序的升級,它們同屬於插入排序類,快速排序其實就是咱們前面認爲最慢的冒泡排序的升級,它們都屬於交換排序類。它也是經過不斷比較和移動交換來實現排序的,只不過它的實現,增大了記錄的比較和移動的距離,快速排序會取一個分界值,將比分界值大的記錄從前面直接移動到後面,比分界值小的記錄從後面直接移動到前面,從而減小了總的比較次數和移動交換次數。程序員

快速排序的思想

快速排序的思想:取一個分界值,經過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的全部數據都比分界值小,另一部分的全部數據比分界值大,而後再按此方法對這兩部分數據分別進行相同操做,整個排序過程能夠遞歸進行,最終達到整個數據變成有序序列。 算法

快速排序動畫演示

快速排序分析

通常沒有特殊要求排序算法都是升序排序,小的在前,大的在後。 數組由{5, 3, 1, 9, 7, 2, 8, 6} 這8個無序元素組成。編程

快速排序步驟:數組

  1. 取一個分界值:咱們暫且拿待排序數據的最前一個元素做爲分界值(樞軸)。 微信

  2. 分區,low到high之間的元素分紅左邊小於樞軸,最右邊大於樞軸。 dom

  3. 分區後小於樞軸和大於樞軸的兩個區域再進行分區,依次類推直到每一個分區數據知足左邊小於樞軸,右邊大於樞軸,排序完成。 性能

最終結果,以下圖: 大數據

快速排序須要解決的兩個問題:優化

  1. 分區後還須要分區可使用遞歸。
  2. 分區時如何讓小於樞軸的數據放到樞軸左邊,大於樞軸的數據放到樞軸的右邊。 使用兩個指針(i, j),i是用來找小於樞軸的數據,j是用來找大於樞軸的數據。 i. 循環查找到須要換位置的數據,進行換位置。 ii. 當i索引的數據大於樞軸,這個數據須要換位置,記錄i的值,中止查找。 iii. 當j索引的數據小於樞軸,這個數據須要換位置,記錄j的值,中止查找。 iv. 若是i > j說明已經找完了,退出循環。 v. 讓i和j位置的元素換位置,i++,指針向右移動繼續找大於樞軸的數據,j--向左移動繼續找小於樞軸的數據。過程以下動畫所示:

快速排序代碼編寫

代碼說明:

  1. void quickSort(int[] arr)方法:用於快速排序的方法,參數爲須要排序的數組。
  2. void qSort(int[] arr, int low, int high)方法:用於將數組指定範圍的數據進行快速排序,此方法不暴露給用戶使用。
  3. int partition(int[] arr, int low, int high)方法:快速排序的分區,將low到high之間的元素分紅左邊小於樞軸,最右邊大於樞軸,返回樞軸的位置。
  4. void swap(int[] arr, int start, int end)方法:將arr數組start索引和end索引的元素進行交換位置。

快速排序代碼以下:

public class QuickSortTest {
    public static void main(String[] args) {
        int[] arr = new int[] {5, 3, 1, 9, 7, 2, 8, 6};
        quickSort(arr);

        System.out.println("排序後:" + Arrays.toString(arr));
    }

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

    // 對arr數組的[low, right]部分進行快速排序
    private static void qSort(int[] arr, int low, int high) {
        if (low >= high) return;
        // 快速排序的分區,將low到high之間的元素分紅左邊小於樞軸,最右邊大於樞軸
        int pivot = partition(arr, low, high);

        // 再次對樞軸左邊和右邊的數據進行分區。
        qSort(arr, low, pivot - 1);
        qSort(arr,pivot + 1, high);
    }

    // 快速排序的分區,將low到high之間的元素分紅左邊小於樞軸,最右邊大於樞軸,返回樞軸的位置。
    private static int partition(int[] arr, int low, int high) {
        // 將第一個元素做爲樞軸
        int v = arr[low];

        int i = low + 1; // arr[low+1, i) <= v; arr(j, high] >= v
        int j = high;

        while (true) {
            // 從左邊找到大於樞軸的數據
            while (i <= high && arr[i] < v) {
                i++;
            }

            // 從右邊找到小於樞軸的數據
            while (j >= low+1 && arr[j] > v) {
                j--;
            }
            if (i > j) break;

            swap(arr, i, j); // 交換i和j位置的元素
            i++; // 左邊的指針向右移動繼續找大於樞軸的數據
            j--; // 右邊的指針向左移動繼續找小於樞軸的數據
        }

        // 交換樞軸到j索引,保證樞軸左邊的元素小於樞軸,樞軸右邊元素大於樞軸。
        swap(arr, low, j);
        return j;
    }

    // 數組兩個元素交換
    public static void swap(int[] arr, int start, int end) {
        if (start == end)
            return;

        int temp = arr[start];
        arr[start] = arr[end];
        arr[end] = temp;
    }
}

快速排序代碼優化1

優化樞軸的選取

咱們知道快速會不斷對數據進行分區,選定一個樞軸,將小於樞軸的數據放到左邊,大於樞軸的數據放到右邊。

前面咱們在對數據進行分區時,都是以數組最前面一個元素做爲樞軸,樞軸的選取不夠合理。這樣會存在一個問題,當數據自己近乎有序時好比數據爲:{1, 2, 3, 5, 6, 7, 9, 8},分區時選擇最左邊的數據做爲樞軸,剛好是數組最小或最大數據,致使分區時,數據都在數軸一側會致使快速排序退化爲一個O(n2)的算法。

如何選取樞軸纔不會讓近乎有序的數據排序退化成O(n^2)呢,咱們能夠看到緣由是咱們一直選取數組最前面的一個數據做爲樞軸,所以咱們能夠隨機選取一個元素做爲數軸,這樣,每次都選取到最大或最小的機率就會很是低。改進後的代碼以下:

public class QuickSortTest2 {
    public static void main(String[] args) {
        int[] arr = new int[] {5, 3, 1, 9, 7, 2, 8, 6};
        quickSort(arr);

        System.out.println("排序後:" + Arrays.toString(arr));
    }

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

    // 對arr數組的[low, right]部分進行快速排序
    public static void qSort(int[] arr, int low, int high) {
        if (low >= high) return;
        // 快速排序的分區,將low到high之間的元素分紅左邊小於樞軸,最右邊大於樞軸
        int pivot = partition2(arr, low, high);

        // 再次對樞軸左邊和右邊的數據進行分區。
        qSort(arr, low, pivot - 1);
        qSort(arr,pivot + 1, high);
    }

    // 數組兩個元素交換
    public static void swap(int[] arr, int start, int end) {
        if (start == end)
            return;

        int temp = arr[start];
        arr[start] = arr[end];
        arr[end] = temp;
    }

    // 快速排序的分區,將low到high之間的元素分紅左邊小於樞軸,最右邊大於樞軸,返回樞軸的位置。
    private static int partition2(int[] arr, int low, int high) {
        // 將第一個元素做爲樞軸,若是數組是近乎有序的數組,那麼每次使用第一個元素拆分,會讓拆分傾斜到一邊,很是的不平衡.
        // 修改爲隨機選取一個元素做爲數軸
        Random ran = new Random();
        // 獲得隨機的索引
        int rIndex = ran.nextInt(10000000) % (high - low + 1) + low;
        // 拿這個隨機索引的數據和最前面的數據交換,這個隨機的數據做爲數軸
        swap(arr, low, rIndex);

        // 將第一個元素做爲樞軸
        int v = arr[low];

        int i = low + 1; // arr[low+1, i) <= v; arr(j, high] >= v
        int j = high;

        while (true) {
            // 從左邊找到大於樞軸的數據
            while (i <= high && arr[i] < v) {
                i++;
            }

            // 從右邊找到小於樞軸的數據
            while (j >= low+1 && arr[j] > v) {
                j--;
            }
            if (i > j) break;

            swap(arr, i, j); // 交換i和j位置的元素
            i++; // 左邊的指針向右移動繼續找大於樞軸的數據
            j--; // 右邊的指針向左移動繼續找小於樞軸的數據
        }

        // 交換樞軸到j索引,保證樞軸左邊的元素小於樞軸,樞軸右邊元素大於樞軸。
        swap(arr, low, j);
        return j;
    }
}

快速排序代碼優化2

小數據量使用插入排序

如今咱們的快速排序是一直分區,直到分區中的每一個元素都有序,咱們知道插入排序在數據量小時效率相對較高,當元素數量較少時,咱們可使用插入排序來替換繼續分區,從而提升插入排序的效率,優化後代碼以下:

public class QuickSortTest3 {
    public static void main(String[] args) {
        int[] arr = new int[] {5, 3, 1, 9, 7, 2, 8, 6};
        quickSort(arr);

        System.out.println("排序後:" + Arrays.toString(arr));
    }

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

    // 對arr數組的[low, right]部分進行快速排序
    public static void qSort(int[] arr, int low, int high) {
        if (low >= high) return;

        if (high - low <= 15) {
            insertionSort(arr, low, high);
            return;
        }

        // 快速排序的分區,將low到high之間的元素分紅左邊小於樞軸,最右邊大於樞軸
        int pivot = partition2(arr, low, high);

        // 再次對樞軸左邊和右邊的數據進行分區。
        qSort(arr, low, pivot - 1);
        qSort(arr,pivot + 1, high);
    }

    // 對數組指定索引範圍的元素使用插入排序
    public static void insertionSort(int[] arr, int low, int high) {
        for (int i = low + 1; i <= high; i++) {
            int e = arr[i]; // 獲得當前這個要插入的元素
            int j;
            for (j = i; j > low && arr[j-1] > e; j--) {
                arr[j] = arr[j-1];
            }
            arr[j] = e;
        }
    }

    // 快速排序的分區,將low到high之間的元素分紅左邊小於樞軸,最右邊大於樞軸,返回樞軸的位置。
    private static int partition2(int[] arr, int low, int high) {
        // 將第一個元素做爲樞軸,若是數組是近乎有序的數組,那麼每次使用第一個元素拆分,會讓拆分傾斜到一邊,很是的不平衡.
        // 修改爲隨機選取一個元素做爲數軸
        Random ran = new Random();
        // 獲得隨機的索引
        int rIndex = ran.nextInt(10000000) % (high - low + 1) + low;
        // 拿這個隨機索引的數據和最前面的數據交換,這個隨機的數據做爲數軸
        swap(arr, low, rIndex);

        // 將第一個元素做爲樞軸
        int v = arr[low];

        int i = low + 1; // arr[low+1, i) <= v; arr(j, high] >= v
        int j = high;

        while (true) {
            // 從左邊找到大於樞軸的數據
            while (i <= high && arr[i] < v) {
                i++;
            }

            // 從右邊找到小於樞軸的數據
            while (j >= low+1 && arr[j] > v) {
                j--;
            }
            if (i > j) break;

            swap(arr, i, j); // 交換i和j位置的元素
            i++; // 左邊的指針向右移動繼續找大於樞軸的數據
            j--; // 右邊的指針向左移動繼續找小於樞軸的數據
        }

        // 交換樞軸到j索引,保證樞軸左邊的元素小於樞軸,樞軸右邊元素大於樞軸。
        swap(arr, low, j);
        return j;
    }

    // 數組兩個元素交換
    public static void swap(int[] arr, int start, int end) {
        if (start == end)
            return;

        int temp = arr[start];
        arr[start] = arr[end];
        arr[end] = temp;
    }
}

快速排序代碼優化3

3路快速排序

前面咱們在進行分區時,大量和樞軸重複的數據還會進入下一次排序。代碼以下:

private static int partition2(int[] arr, int low, int high) {
    ...
    while (true) {
        // 從左邊找到大於樞軸的數據
        while (i <= high && arr[i] < v) {
            i++;
        }
        // 從右邊找到小於樞軸的數據
        while (j >= low+1 && arr[j] > v) {
            j--;
        }
        if (i > j) break;
        swap(arr, i, j); // 交換i和j位置的元素
        i++; // 左邊的指針向右移動繼續找大於樞軸的數據
        j--; // 右邊的指針向左移動繼續找小於樞軸的數據
    }
    ...
}

進行一次分區後,大量和樞軸重複的數據還會進入下一次排序,浪費性能,和樞軸相同的數據不用再進入下次分區。效果以下:

所以咱們在進行分區時,能夠將數據分紅3個區域,小於樞軸的數據,等於數軸的數據,大於樞軸的數據,這樣處理的好處是等於樞軸的數據不會進入下一次分區,因此在待排序數據中出現大量重複數據時能夠提升效率。

優化後代碼:

public class QuickSortTest4 {
    public static void main(String[] args) {
        int[] arr = new int[] {5, 3, 1, 9, 7, 2, 8, 6};
        quickSort3Ways(arr);

        System.out.println("排序後:" + Arrays.toString(arr));
    }

    public static void quickSort3Ways(int[] arr) {
        qSort3Ways(arr, 0, arr.length-1);
    }

    // 對arr數組的[low, right]部分進行快速排序
    public static void qSort3Ways(int[] arr, int low, int high) {
        if (low >= high) return;

        if (high - low <= 15) {
            insertionSort(arr, low, high);
            return;
        }

        int pivot = partition3Ways(arr, low, high);
        qSort3Ways(arr, low, pivot - 1);
        qSort3Ways(arr,pivot + 1, high);
    }

    private static int partition3Ways(int[] arr, int low, int high) {
        // 將第一個元素做爲樞軸,若是數組是近乎有序的數組,那麼每次使用第一個元素拆分,會讓拆分傾斜到一邊,很是的不平衡.
        // 修改爲隨機選取一個數字進行拆分
        Random ran = new Random();
        int rIndex = ran.nextInt(10000000) % (high - low + 1) + low;
        swap(arr, low, rIndex);
        int v = arr[low];

        int lt = low;     // arr[l+1...lt] < v
        int gt = high + 1; // arr[gt...r] > v
        int i = low+1;    // arr[lt+1...i) == v

        while (i < gt) {
            if (arr[i] < v) {
                swap(arr, i, lt+1);
                i++;
                lt++;
            } else if (arr[i] > v) {
                swap(arr, i, gt-1);
                gt--;
            } else {
                i++;
            }
        }
        swap(arr, low, lt);
        return lt;
    }

    // 對數組指定索引範圍的元素使用插入排序
    public static void insertionSort(int[] arr, int low, int high) {
        for (int i = low + 1; i <= high; i++) {
            int e = arr[i]; // 獲得當前這個要插入的元素
            int j;
            for (j = i; j > low && arr[j-1] > e; j--) {
                arr[j] = arr[j-1];
            }
            arr[j] = e;
        }
    }

    // 數組兩個元素交換
    public static void swap(int[] arr, int start, int end) {
        if (start == end)
            return;

        int temp = arr[start];
        arr[start] = arr[end];
        arr[end] = temp;
    }
}

快速排序代碼優化4

減小遞歸次數

咱們知道,遞歸對性能有必定影響,上面的qSort3Ways方法內部先進行分區,而後進行兩次遞歸。若是待排序的序列劃分極端不平衡,遞歸深度將趨近於n,而不是平衡時的log2n, 除了分區次數變多,影響排序效率以外。棧的大小是頗有限的,每次遞歸調用都會耗費必定的棧空間,所以減小遞歸,能夠提升性能,而且防止棧空間不足而致使的棧溢出問題。 減小遞歸次數後的代碼以下:

public class QuickSortTest5 {
    public static void main(String[] args) {
        int[] arr = new int[] {5, 3, 1, 9, 7, 2, 8, 6};
        quickSort3Ways(arr);

        System.out.println("排序後:" + Arrays.toString(arr));
        System.out.println("我被遞歸了:" + count);
    }

    public static void quickSort3Ways(int[] arr) {
        qSort3Ways(arr, 0, arr.length-1);
    }

    private static int count;
    // 對arr數組的[low, right]部分進行快速排序
    public static void qSort3Ways(int[] arr, int low, int high) {
        if (low >= high) return;
        count++;
        if (high - low <= 15) {
            insertionSort(arr, low, high);
        } else {
            while (low < high) {
                int pivot = partition3Ways(arr, low, high);
                qSort3Ways(arr, low, pivot - 1);
                low = pivot + 1; // 循環會對右邊的區域進行分區,而不是遞歸再對右邊進行分區
            }
        }
    }

    private static int partition3Ways(int[] arr, int low, int high) {
        // 將第一個元素做爲樞軸,若是數組是近乎有序的數組,那麼每次使用第一個元素拆分,會讓拆分傾斜到一邊,很是的不平衡.
        // 修改爲隨機選取一個數字進行拆分
        Random ran = new Random();
        int rIndex = ran.nextInt(10000000) % (high - low + 1) + low;
        swap(arr, low, rIndex);
        int v = arr[low];

        int lt = low;     // arr[l+1...lt] < v
        int gt = high + 1; // arr[gt...r] > v
        int i = low+1;    // arr[lt+1...i) == v

        while (i < gt) {
            if (arr[i] < v) {
                swap(arr, i, lt+1);
                i++;
                lt++;
            } else if (arr[i] > v) {
                swap(arr, i, gt-1);
                gt--;
            } else {
                i++;
            }
        }
        swap(arr, low, lt);
        return lt;
    }

    // 對數組指定索引範圍的元素使用插入排序
    public static void insertionSort(int[] arr, int low, int high) {
        for (int i = low + 1; i <= high; i++) {
            int e = arr[i]; // 獲得當前這個要插入的元素
            int j;
            for (j = i; j > low && arr[j-1] > e; j--) {
                arr[j] = arr[j-1];
            }
            arr[j] = e;
        }
    }

    // 數組兩個元素交換
    public static void swap(int[] arr, int start, int end) {
        if (start == end)
            return;

        int temp = arr[start];
        arr[start] = arr[end];
        arr[end] = temp;
    }
}

總結

  1. 快速排序其實就是咱們前面認爲最慢的冒泡排序的升級,它們都屬於交換排序類。
  2. 快速排序的思想:取一個分界值,經過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的全部數據都比分界值小,另一部分的全部數據比分界值大,而後再按此方法對這兩部分數據分別進行相同操做,整個排序過程能夠遞歸進行,最終達到整個數據變成有序序列。
  3. 快速排序代碼優化1:優化樞軸的選取。
  4. 快速排序代碼優化2:小數據量使用插入排序。
  5. 快速排序代碼優化3: 3路快速排序。
  6. 快速排序代碼優化4:減小遞歸次數。

快速排序算法被列爲20世紀十大算法之一,通過屢次的優化後,在總體性能上,依然是排序算法王者,快速排序是程序員必須掌握的一種排序算法。


原創文章和動畫製做真心不易,您的點贊就是最大的支持! 想了解更多文章請關注微信公衆號:表哥動畫學編程

相關文章
相關標籤/搜索