內容介紹
快速排序簡介
快速排序(Quicksort)是對冒泡排序的一種改進。快速排序由C. A. R. Hoare在1960年提出。快速排序算法被列爲20世紀十大算法之一,這足以說明的他的做用和重要性。快速排序是程序員必須掌握的一種排序算法。java
希爾排序至關於直接插入排序的升級,它們同屬於插入排序類,快速排序其實就是咱們前面認爲最慢的冒泡排序的升級,它們都屬於交換排序類。它也是經過不斷比較和移動交換來實現排序的,只不過它的實現,增大了記錄的比較和移動的距離,快速排序會取一個分界值,將比分界值大的記錄從前面直接移動到後面,比分界值小的記錄從後面直接移動到前面,從而減小了總的比較次數和移動交換次數。程序員
快速排序的思想
快速排序的思想:取一個分界值,經過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的全部數據都比分界值小,另一部分的全部數據比分界值大,而後再按此方法對這兩部分數據分別進行相同操做,整個排序過程能夠遞歸進行,最終達到整個數據變成有序序列。 算法
快速排序動畫演示
快速排序分析
通常沒有特殊要求排序算法都是升序排序,小的在前,大的在後。 數組由{5, 3, 1, 9, 7, 2, 8, 6} 這8個無序元素組成。編程
快速排序步驟:數組
-
取一個分界值:咱們暫且拿待排序數據的最前一個元素做爲分界值(樞軸)。
微信
-
分區,low到high之間的元素分紅左邊小於樞軸,最右邊大於樞軸。
dom
-
分區後小於樞軸和大於樞軸的兩個區域再進行分區,依次類推直到每一個分區數據知足左邊小於樞軸,右邊大於樞軸,排序完成。
性能
最終結果,以下圖: 大數據
快速排序須要解決的兩個問題:優化
- 分區後還須要分區可使用遞歸。
- 分區時如何讓小於樞軸的數據放到樞軸左邊,大於樞軸的數據放到樞軸的右邊。 使用兩個指針(
i
,j
),i
是用來找小於樞軸的數據,j
是用來找大於樞軸的數據。 i. 循環查找到須要換位置的數據,進行換位置。 ii. 當i索引的數據大於樞軸,這個數據須要換位置,記錄i的值,中止查找。 iii. 當j索引的數據小於樞軸,這個數據須要換位置,記錄j的值,中止查找。 iv. 若是i > j說明已經找完了,退出循環。 v. 讓i和j位置的元素換位置,i++,指針向右移動繼續找大於樞軸的數據,j--向左移動繼續找小於樞軸的數據。過程以下動畫所示:
快速排序代碼編寫
代碼說明:
void quickSort(int[] arr)
方法:用於快速排序的方法,參數爲須要排序的數組。void qSort(int[] arr, int low, int high)
方法:用於將數組指定範圍的數據進行快速排序,此方法不暴露給用戶使用。int partition(int[] arr, int low, int high)
方法:快速排序的分區,將low到high之間的元素分紅左邊小於樞軸,最右邊大於樞軸,返回樞軸的位置。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: 3路快速排序。
- 快速排序代碼優化4:減小遞歸次數。
快速排序算法被列爲20世紀十大算法之一,通過屢次的優化後,在總體性能上,依然是排序算法王者,快速排序是程序員必須掌握的一種排序算法。
原創文章和動畫製做真心不易,您的點贊就是最大的支持! 想了解更多文章請關注微信公衆號:表哥動畫學編程