日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

排序算法 - 面试中的排序算法总结

發(fā)布時間:2024/2/28 编程问答 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 排序算法 - 面试中的排序算法总结 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

排序算法總結(jié)

查找和排序算法是算法的入門知識,其經(jīng)典思想可以用于很多算法當(dāng)中。因?yàn)槠鋵?shí)現(xiàn)代碼較短,應(yīng)用較常見。所以在面試中經(jīng)常會問到排序算法及其相關(guān)的問題。但萬變不離其宗,只要熟悉了思想,靈活運(yùn)用也不是難事。

一般在面試中最常考的是快速排序歸并排序,并且經(jīng)常有面試官要求現(xiàn)場寫出這兩種排序的代碼。對這兩種排序的代碼一定要信手拈來才行。還有插入排序、冒泡排序、堆排序、基數(shù)排序、桶排序等。面試官對于這些排序可能會要求比較各自的優(yōu)劣、各種算法的思想及其使用場景,分析算法的時間和空間復(fù)雜度


排序算法分類、性能比較及使用場景

一、排序算法種類

我們通常所說的排序算法往往指的是內(nèi)部排序算法,即數(shù)據(jù)記錄在內(nèi)存中進(jìn)行排序。

排序算法大體可分為兩種:

  • 比較排序:時間復(fù)雜度最少可達(dá)到O(nlogn),主要有:冒泡排序,選擇排序,插入排序,歸并排序,堆排序,快速排序等。
  • 非比較排序:時間復(fù)雜度可以達(dá)到O(n),主要有:計數(shù)排序,基數(shù)排序,桶排序等。

二、性能比較

下表給出了常見比較排序算法的性能

排序算法穩(wěn)定性
1) 穩(wěn)定的:如果存在多個具有相同排序碼的記錄,經(jīng)過排序后,這些記錄的相對次序仍然保持不變,則這種排序算法稱為穩(wěn)定的。
插入排序、冒泡排序、歸并排序、非比較排序(基數(shù)、計數(shù)、桶式)都是穩(wěn)定的排序算法。
2)不穩(wěn)定的:直接選擇排序、堆排序、希爾排序、快速排序。

三、使用場景

  • 若n較小(如n≤50),可采用直接插入或直接選擇排序。當(dāng)記錄規(guī)模較小時,直接插入排序較好;否則因?yàn)橹苯舆x擇移動的記錄數(shù)少于直接插人,應(yīng)選直接選擇排序?yàn)橐恕?/li>
  • 若文件初始狀態(tài)基本有序(指正序),則應(yīng)選用直接插入、冒泡或隨機(jī)的快速排序?yàn)橐?#xff1b;
  • 若n較大,則應(yīng)采用時間復(fù)雜度為O(nlgn)的排序方法:快速排序、堆排序或歸并排序。
    • 快速排序是目前基于比較的內(nèi)部排序中被認(rèn)為是最好的方法,當(dāng)待排序的關(guān)鍵字是隨機(jī)分布時,快速排序的平均時間最短;
    • 堆排序所需的輔助空間少于快速排序,并且不會出現(xiàn)快速排序可能出現(xiàn)的最壞情況。這兩種排序都是不穩(wěn)定的。
    • 若要求排序穩(wěn)定,則可選用歸并排序。通常可以將它和直接插入排序結(jié)合在一起使用:先利用直接插入排序求得較長的有序子文件,然后再兩兩歸并之。因?yàn)橹苯硬迦肱判蚴欠€(wěn)定 的,所以改進(jìn)后的歸并排序仍是穩(wěn)定的。

首先定義了一個Swap類,并且實(shí)現(xiàn)靜態(tài)方法swap用于交換數(shù)組指定位置上的元素值。

/** * 交換數(shù)組指定位置上兩個數(shù)的值 * @param arr 數(shù)組 * @param i 第一個數(shù)字在數(shù)組的下標(biāo) * @param j 第二個數(shù)字在數(shù)組的下標(biāo) */ public static void swap(int[] arr, int i, int j) {int temp = arr[i];arr[i] = arr[j];arr[j] = temp; }

下面是各個排序算法的具體思想和實(shí)現(xiàn)

簡單排序算法

冒泡排序

冒泡排序是最簡單的排序之一了,其大體思想就是通過與相鄰元素的比較和交換來把小的數(shù)交換到最前面。這個過程類似于水泡向上升一樣,因此而得名。

舉個例子,對5,3,8,6,4這個無序序列進(jìn)行冒泡排序。首先從后向前冒泡,4和6比較,把4交換到前面,序列變成5,3,8,4,6。同理4和8交換,變成5,3,4,8,6,3和4無需交換。5和3交換,變成3,5,4,8,6,3。這樣一次冒泡就完了,把最小的數(shù)3排到最前面了。對剩下的序列依次冒泡就會得到一個有序序列。

冒泡排序的時間復(fù)雜度為:O(n2)

算法實(shí)現(xiàn)如下(Java):

public static void main(String[] args) {int[] arr = {5, 3, 4, 8, 6};bubbleSort(arr);System.out.println(Arrays.toString(arr)); }/*** 冒泡排序:穩(wěn)定。屬于交換排序算法的一種,另外一種是快速排序。* 時間復(fù)雜度:最好為 O(n^2),最差為 O(n^2),平均為 O(n^2)。空間復(fù)雜度:O(1)* @param arr 數(shù)組*/ public static void bubbleSort(int[] arr){if (arr == null || arr.length == 0) { // 數(shù)組為null或者沒有元素return;}for (int i = 0; i < arr.length - 1; i++) { for (int j = arr.length - 1; j > i; j--) {if (arr[j] < arr[j - 1]) {Swap.swap(arr, j, j - 1);}}} }

選擇排序

思想其實(shí)和冒泡排序有點(diǎn)類似,都是在一次排序后把最小的元素放到最前面。但是過程不同,冒泡排序是通過相鄰的比較和交換。而選擇排序是通過對整體的選擇。

舉個例子,對5,3,8,6,4這個無序序列進(jìn)行簡單選擇排序,首先要選擇5以外的最小數(shù)來和5交換,也就是選擇3和5交換,一次排序后就變成了3,5,8,6,4。對剩下的序列一次進(jìn)行選擇和交換,最終就會得到一個有序序列。其實(shí)選擇排序可以看成冒泡排序的優(yōu)化,因?yàn)槠淠康南嗤?#xff0c;只是選擇排序只有在確定了最小數(shù)的前提下才進(jìn)行交換,大大減少了交換的次數(shù)。

選擇排序的時間復(fù)雜度為:O(n2)

算法實(shí)現(xiàn)如下(Java):

public static void main(String[] args) {int[] arr = {5, 3, 4, 8, 6};selectSort(arr);System.out.println(Arrays.toString(arr)); }/*** 選擇排序:不穩(wěn)定。另外一種是堆排序。* 時間復(fù)雜度:最好為 O(n^2),最差為 O(n^2),平均為 O(n^2)。空間復(fù)雜度:O(1)* @param arr*/ public static void selectSort(int[] arr){if (arr == null || arr.length == 0) {return;}int minIndex = 0; // 記錄每次循環(huán)最小值得數(shù)組下標(biāo),在循環(huán)外創(chuàng)建節(jié)省空間for (int i = 0; i < arr.length - 1; i++) { // 只需要比較n-1次minIndex = i; // 每次初始化為當(dāng)前位置for (int j = i + 1; j < arr.length; j++) {if (arr[j] < arr[minIndex]) {minIndex = j; // 保證minIndex對應(yīng)的元素為這次循環(huán)中的最小元素}}if (minIndex != i) { // 如果minIndex不為i,說明找到了更小的值,交換之Swap.swap(arr, i, minIndex);}} }

插入排序

不是通過交換位置而是通過比較找到合適的位置插入元素來達(dá)到排序的目的的。相信大家都有過打撲克牌的經(jīng)歷,特別是牌數(shù)較大的。在分牌時可能要整理自己的牌,牌多的時候怎么整理呢? 就是拿到一張牌,找到一個合適的位置插入。這個原理其實(shí)和插入排序是一樣的。

舉個例子,對5,3,8,6,4這個無序序列進(jìn)行簡單插入排序,首先假設(shè)第一個數(shù)的位置時正確的,想一下在拿到第一張牌的時候,沒必要整理。然后3要插到5前面,把5后移一位,變成3,5,8,6,4.想一下整理牌的時候應(yīng)該也是這樣吧。然后8不用動,6插在8前面,8后移一位,4插在5前面,從5開始都向后移一位。注意在插入一個數(shù)的時候要保證這個數(shù)前面的數(shù)已經(jīng)有序。

簡單插入排序的時間復(fù)雜度為:O(n2)

算法實(shí)現(xiàn)如下(Java):

public static void main(String[] args) {int[] arr = {5, 3, 4, 8, 6};insertSort(arr);System.out.println(Arrays.toString(arr)); }/*** 插入排序:穩(wěn)定。另外一種是希爾排序。* 時間復(fù)雜度:最好為 O(n^2),最差為 O(n^2),平均為 O(n^2)。空間復(fù)雜度:O(1)* @param arr 數(shù)組*/ public static void insertSort(int[] arr){if (arr == null || arr.length == 0) {return;}for (int i = 1; i < arr.length; i++) { // 假設(shè)第一個數(shù)位置是正確的int j = i;int target = arr[j]; // 待插入的值// 將大于target的元素往后移動,直到小于target的元素while (j > 0 && arr[j - 1] > target) {arr[j] = arr[j - 1];j--;}if (j != i) {arr[j] = target; // 插入target數(shù)字到數(shù)組中}} }

復(fù)雜排序算法

快速排序

通過一趟排序?qū)⒋庞涗浄指畛瑟?dú)立的兩部分,其中一部記錄的關(guān)鍵均比另一部分記錄的關(guān)鍵字小,則可分別對這兩部分記錄繼續(xù)進(jìn)行排序。快速排序一聽名字就覺得很高端,在實(shí)際應(yīng)用當(dāng)中快速排序確實(shí)也是表現(xiàn)最好的排序算法。

快速排序雖然高端,但其實(shí)其思想是來自冒泡排序,冒泡排序是通過相鄰元素的比較和交換把最小的冒泡到最頂端,而快速排序是比較和交換小數(shù)和大數(shù),這樣一來不僅把小數(shù)冒泡到上面同時也把大數(shù)沉到下面。

舉個例子:對5,3,8,6,4這個無序序列進(jìn)行快速排序,思路是右指針找比基準(zhǔn)數(shù)小的,左指針找比基準(zhǔn)數(shù)大的,交換之。5,3,8,6,4 用5作為比較的基準(zhǔn),最終會把5小的移動到5的左邊,比5大的移動到5的右邊。5,3,8,6,4 首先設(shè)置i,j兩個指針分別指向兩端,j指針先掃描(思考一下為什么?)4比5小停止。然后i掃描,8比5大停止。交換i,j位置。5,3,4,6,8 然后j指針再掃描,這時j掃描4時兩指針相遇。停止。然后交換4和基準(zhǔn)數(shù)。4,3,5,6,8 一次劃分后達(dá)到了左邊比5小,右邊比5大的目的。之后對左右子序列遞歸排序,最終得到有序序列。

上面留下來了一個問題為什么一定要j指針先動呢?首先這也不是絕對的,這取決于基準(zhǔn)數(shù)的位置,因?yàn)樵谧詈髢蓚€指針相遇的時候,要交換基準(zhǔn)數(shù)到相遇的位置。一般選取第一個數(shù)作為基準(zhǔn)數(shù),那么就是在左邊,所以最后相遇的數(shù)要和基準(zhǔn)數(shù)交換,那么相遇的數(shù)一定要比基準(zhǔn)數(shù)小。所以j指針先移動才能先找到比基準(zhǔn)數(shù)小的數(shù)。

快速排序是不穩(wěn)定的,其時間平均時間復(fù)雜度是:O(nlgn)

總結(jié)快速排序的思想:冒泡 + 二分 + 遞歸分治,慢慢體會。

public static void main(String[] args) {int[] arr = {5, 3, 4, 8, 2}; // sort(arr);quickSortNonRec(arr);System.out.println(Arrays.toString(arr)); }/** * 快速排序:不穩(wěn)定。冒泡 + 二分 + 遞歸分治。 * 時間復(fù)雜度:最好為 O(nlogn),最差為 O(n^2),平均為 O(nlogn)。空間復(fù)雜度:O(logn) * @param arr 數(shù)組 */ public static void sort(int[] arr) {if (arr == null || arr.length == 0) {return;}quickSort(arr, 0, arr.length - 1); }/** * 快速排序 * @param arr 數(shù)組 * @param left 左指針 * @param right 右指針 */ public static void quickSort(int[] arr, int left, int right) {if (left > right) {return;}int pos = partition(arr, left, right); // 每次得到基準(zhǔn)值的位置quickSort(arr, left, pos - 1); // 分別對數(shù)組的左右子數(shù)組重復(fù)快排調(diào)用quickSort(arr, pos + 1, right); }/** * 每次得到基準(zhǔn)值下標(biāo),并進(jìn)行一次排序 * @param arr 數(shù)組 * @param left 左指針 * @param right 右指針 * @return 基準(zhǔn)值下標(biāo) */ public static int partition(int[] arr, int left, int right) {int temp = arr[left]; // 保存基準(zhǔn)值while (left < right) {while (left < right && arr[right] > temp) { // 從右向左開始循環(huán)遍歷right--;}arr[left] = arr[right]; // 左邊的值都比基準(zhǔn)值小while (left < right && arr[left] < temp) { // 從左向右開始循環(huán)遍歷left++;}arr[right] = arr[left]; // 右邊的值都比基準(zhǔn)值大}arr[left] = temp;return left; }/** * 非遞歸實(shí)現(xiàn) * @param arr 數(shù)組 */ public static void quickSortNonRec(int[] arr) {if (arr == null || arr.length == 0) {return;}Stack<Integer> stack = new Stack<>(); // 用一個棧保存每次循環(huán)的子數(shù)組范圍stack.add(0);stack.add(arr.length - 1);while (!stack.isEmpty()) {int right = stack.pop();int left = stack.pop();int pos = partition(arr, left, right);if (pos - 1 > left) {stack.add(left);stack.add(pos - 1);}if (pos + 1 < right) {stack.add(pos + 1);stack.add(right);}} }

歸并排序

歸并排序是另一種不同的排序方法,因?yàn)闅w并排序使用了遞歸分治的思想,所以理解起來比較容易。

基本思想:先遞歸劃分子問題,然后合并結(jié)果。把待排序列看成由兩個有序的子序列,然后合并兩個子序列,然后把子序列看成由兩個有序序列。倒著來看,其實(shí)就是先兩兩合并,然后四四合并,最終形成有序序列。

空間復(fù)雜度為:O(n),時間復(fù)雜度為:O(nlogn)

public static void main(String[] args) {int[] arr = {5, 3, 4, 8, 2}; // mergeSort(arr);mergeSortNonRec(arr);System.out.println(Arrays.toString(arr)); }/*** 歸并排序:穩(wěn)定。遞歸分治的實(shí)現(xiàn)。* 時間復(fù)雜度:最好為 O(nlogn),最差為 O(nlogn),平均為 O(nlogn)。空間復(fù)雜度:O(n)* @param arr 數(shù)組*/ public static void mergeSort(int[] arr){if (arr == null || arr.length == 0) {return;}mSort(arr, 0, arr.length - 1); // 歸并排序 }/*** 歸并排序:遞歸分治* @param arr 數(shù)組* @param left 歸并起始下標(biāo)* @param right 歸并結(jié)束下標(biāo)*/ public static void mSort(int[] arr, int left, int right) {if (left >= right) {return;}int mid = (left + right) / 2; // 中間位置mSort(arr, left, mid); // 左半邊排序 【left, mid】mSort(arr, mid + 1, right); // 右半邊排序 【mid + 1, right】merge(arr, left, mid, right); // 合并左右兩邊的排序結(jié)果 }/*** 合并一趟歸并后的數(shù)組* @param arr 數(shù)組* @param left 左下標(biāo)* @param mid 中間下標(biāo)* @param right 右下標(biāo)*/ public static void merge(int[] arr, int left, int mid, int right) {int[] temp = new int[right - left + 1]; // 臨時數(shù)組,保存合并后的兩個數(shù)組int k = 0;int i = left; // 左半邊數(shù)組起始位置int j = mid + 1; // 右半邊數(shù)組起始位置while (i <= mid && j <= right) {if (arr[i] < arr[j]) { // 每次保存比較小的那個數(shù)temp[k++] = arr[i++];} else {temp[k++] = arr[j++];}}// 如果前半個數(shù)組沒有遍歷完while (i <= mid) {temp[k++] = arr[i++];}// 如果后半個數(shù)組沒有遍歷完while (j <= right) {temp[k++] = arr[j++];}// 因?yàn)閍rr的數(shù)組是從left開始到right的,所以更新對應(yīng)的位置上的元素值for (int l = 0; l < temp.length; l++) {arr[l + left] = temp[l];} }/*** 歸并排序非遞歸算法:* 設(shè)置每次歸并長度為2的指數(shù)式增長* 每個歸并長度內(nèi)對整個數(shù)組進(jìn)行歸并* @param arr 數(shù)組*/ public static void mergeSortNonRec(int[] arr) {if (arr == null || arr.length == 0) {return;}int len = 1; // 每次歸并的長度while (len < arr.length) {for (int i = 0; i < arr.length; i += 2 * len) { // 將len歸并規(guī)模的數(shù)組進(jìn)行一次歸并mergeSortNonRec(arr, i, len); // 每兩兩數(shù)組進(jìn)行歸并排序}len *= 2; // 將每組歸并元素的規(guī)模擴(kuò)大,1->2->4->8...} }/*** 歸并排序,對數(shù)組元素進(jìn)行歸并排序* @param arr 數(shù)組* @param i 歸并兩個子數(shù)組的起始位置* @param length 歸并兩個子數(shù)組每個子數(shù)組的長度股*/ public static void mergeSortNonRec(int[] arr, int i, int length) {int start = i; // 保存起始位置由于恢復(fù)排序后的數(shù)組int arrLen1 = i + length; // 保存前面一個數(shù)組下標(biāo)最大值int arrLen2 = i + 2 * length; // 保存后面一個數(shù)組下標(biāo)最大值int j = i + length;int[] temp = new int[2 * length]; // 歸并后的數(shù)組int k = 0;while (i < arrLen1 && j < arrLen2 && i < arr.length && j < arr.length) { // 注意i和j可能超出數(shù)組長度,因此需要考慮邊界問題if (arr[i] < arr[j]) {temp[k++] = arr[i++];} else {temp[k++] = arr[j++];}}while (i < arrLen1 && i < arr.length) { // 剩余數(shù)組元素直接拷貝,注意下標(biāo)越界temp[k++] = arr[i++];}while (j < arrLen2 && j < arr.length) { // 剩余數(shù)組元素直接拷貝,注意下標(biāo)越界temp[k++] = arr[j++];}k = 0;while (start < arr.length && k < 2 * length) { // 更新原數(shù)組相應(yīng)位置元素的值為歸并排序后的值arr[start++] = temp[k++];} }

堆排序

堆排序是借助堆來實(shí)現(xiàn)的選擇排序,思想同簡單的選擇排序,以下以大頂堆為例。注意:如果想升序排序就使用大頂堆,反之使用小頂堆。

首先,實(shí)現(xiàn)堆排序需要解決兩個問題:
1. 建堆:如何由一個無序序列鍵成一個堆?
2. 調(diào)整堆:如何在輸出堆頂元素之后,調(diào)整剩余元素成為一個新的堆?

第一個問題,可以直接使用線性數(shù)組來表示一個堆,由初始的無序序列建成一個堆就需要自底向上從第一個非葉元素開始挨個調(diào)整成一個堆。
第二個問題,怎么調(diào)整成堆?首先是將堆頂元素和最后一個元素交換。然后比較當(dāng)前堆頂元素的左右孩子節(jié)點(diǎn),因?yàn)槌水?dāng)前的堆頂元素,左右孩子堆均滿足條件,這時需要選擇當(dāng)前堆頂元素與左右孩子節(jié)點(diǎn)的較大者(大頂堆)交換,直至葉子節(jié)點(diǎn)。我們稱這個自堆頂自葉子的調(diào)整成為篩選。從一個無序序列建堆的過程就是一個反復(fù)篩選的過程。若將此序列看成是一個完全二叉樹,則最后一個非終端節(jié)點(diǎn)是n/2取底個元素,由此篩選即可。

舉個例子,49,38,65,97,76,13,27,49 序列的堆排序建初始堆和調(diào)整的過程如下:

堆排序算法的實(shí)現(xiàn),以大頂堆為例。

public static void main(String[] args) {int[] arr = {5, 3, 4, 8, 2};heapSort(arr);System.out.println(Arrays.toString(arr)); }/*** 堆排序:不穩(wěn)定。選擇排序的變種。* 時間復(fù)雜度:最好為 O(nlogn),最差為 O(nlogn),平均為 O(nlogn)。空間復(fù)雜度:O(1)* @param arr 數(shù)組*/ public static void heapSort(int[] arr) {if (arr == null || arr.length == 0) {return;}// 初始化建立大頂堆for (int i = arr.length / 2; i >= 0; i--) {heapAdjust(arr, i, arr.length - 1);}// 每次進(jìn)行調(diào)整,得到排序的數(shù)組for (int i = arr.length - 1; i >= 0; i--) {Swap.swap(arr, 0, i);heapAdjust(arr, 0, i - 1);} }/*** 堆篩選,除了start之外,start-end均滿足大頂堆的定義。* 調(diào)整之后start-end稱為一個大頂堆* @param arr 待調(diào)整數(shù)組* @param start 起始指針* @param end 結(jié)束指針*/ public static void heapAdjust(int[] arr, int start, int end) {int temp = arr[start];for (int i = 2 * start + 1; i <= end; i *= 2) {// 左右孩子的節(jié)點(diǎn)分別為2*i+1, 2*i+2if (i < end && arr[i] < arr[i + 1]) {i++;}if (temp >= arr[i]) {break; // 已經(jīng)為大頂堆,=保持穩(wěn)定性}arr[start] = arr[i]; // 將子節(jié)點(diǎn)的值上移start = i; // 下一輪篩選}arr[start] = temp; // 插入到正確的位置 }

希爾排序

希爾排序是插入排序的一種高效率的實(shí)現(xiàn),也叫縮小增量排序。簡單的插入排序中,如果待排序列是正序時,時間復(fù)雜度是O(n),如果序列是基本有序的,使用直接插入排序效率就非常高。希爾排序就利用了這個特點(diǎn)。

基本思想:先將整個待排記錄序列分割成為若干子序列分別進(jìn)行直接插入排序,待整個序列中的記錄基本有序時再對全體記錄進(jìn)行一次直接插入排序。
從上述排序過程可見,希爾排序的特點(diǎn)是,子序列的構(gòu)成不是簡單的逐段分割,而是將某個相隔某個增量的記錄組成一個子序列。

如上面的例子,第一趟排序時的增量為5,第二趟排序的增量為3。
由于前兩趟的插入排序中記錄的關(guān)鍵字是和同一子序列中的前一個記錄的關(guān)鍵字進(jìn)行比較,因此關(guān)鍵字較小的記錄就不是一步一步地向前挪動,而是跳躍式地往前移,從而使得進(jìn)行最后一趟排序時,整個序列已經(jīng)做到基本有序, 只要作記錄的少量比較和移動即可。
因此希爾排序的效率要比直接插入排序高。

希爾排序的分析是復(fù)雜的,時間復(fù)雜度是所取增量的函數(shù),這涉及一些數(shù)學(xué)上的難題。但是在大量實(shí)驗(yàn)的基礎(chǔ)上推出當(dāng)n在某個范圍內(nèi)時,時間復(fù)雜度可以達(dá)到O(n1.3)

public static void main(String[] args) {// TODO Auto-generated method stubint[] arr = {5, 3, 4, 8, 2};shellSort(arr);System.out.println(Arrays.toString(arr)); }/*** 希爾排序:不穩(wěn)定。插入排序的變種。* 時間復(fù)雜度:最好為 O(nlogn),最差為 O(nlogn),平均為 O(nlogn)。空間復(fù)雜度:O(1)* @param arr 數(shù)組*/ public static void shellSort(int[] arr) {if (arr == null || arr.length == 0) {return;}int d = arr.length - 1; // 增量while (d >= 1) { // 增量大于等于1shellInsert(arr, d);d /= 2; // 每次增量除以2} }/*** 希爾排序的一趟排序* @param arr 待排數(shù)組* @param d 增量*/ public static void shellInsert(int[] arr, int d){for (int i = d; i < arr.length; i++) {int j = i;int target = arr[i]; // 帶插入的元素while (j > 0 && arr[j - d] > target) { // 前面的數(shù)大于target,需要往后移動arr[j] = arr[j - d];j -= d;}if (j != i) {arr[j] = target; // 插入target數(shù)字到數(shù)組中}} }

高級排序算法

計數(shù)排序

如果在面試中有面試官要求你寫一個O(n)時間復(fù)雜度的排序算法,你千萬不要立刻說:這不可能!雖然前面基于比較的排序的下限是O(nlogn)。

但是確實(shí)也有線性時間復(fù)雜度的排序,只不過有前提條件,就是待排序的數(shù)要滿足一定的范圍的整數(shù),而且計數(shù)排序需要比較多的輔助空間

其基本思想是,用待排序的數(shù)作為計數(shù)數(shù)組的下標(biāo),統(tǒng)計每個數(shù)字的個數(shù)。然后依次輸出即可得到有序序列。

public static void main(String[] args) {int[] arr = {5, 3, 4, 8, 2};countSort(arr);System.out.println(Arrays.toString(arr)); }/*** 計數(shù)排序:穩(wěn)定。計數(shù)數(shù)組的下標(biāo)為元素的值,計數(shù)數(shù)組的元素為原數(shù)組下標(biāo)對應(yīng)元素的出現(xiàn)次數(shù)。* 時間復(fù)雜度:最好為 O(n+k),最差為 O(n+k),平均為 O(n+k)。空間復(fù)雜度:O(n+k)* @param arr 數(shù)組*/ public static void countSort(int[] arr) {if (arr == null || arr.length == 0) {return;}int max = max(arr);int[] countArr = new int[max + 1]; // 計數(shù)數(shù)組,長度為max+1,這樣countArr[max]可以被賦值for (int i = 0; i < arr.length; i++) {countArr[arr[i]]++;}int k = 0; // arr新數(shù)組的下標(biāo)for (int i = 0; i < countArr.length; i++) {for (int j = 0; j < countArr[i]; j++) {arr[k++] = i; // 數(shù)組的下標(biāo)i對應(yīng)著元素,countArr[i]對應(yīng)著元素出現(xiàn)的個數(shù)}} }/*** 得到數(shù)組元素的最大值* @param arr 數(shù)組* @return 最大值*/ public static int max(int[] arr) {int max = Integer.MIN_VALUE;for (int i = 0; i < arr.length; i++) {if (arr[i] > max) {max = arr[i];}}return max; }

基數(shù)排序

基數(shù)排序又是一種和前面排序方式不同的排序方式,基數(shù)排序不需要進(jìn)行記錄關(guān)鍵字之間的比較。

基數(shù)排序是一種借助多關(guān)鍵字排序思想對單邏輯關(guān)鍵字進(jìn)行排序的方法。所謂的多關(guān)鍵字排序就是有多個優(yōu)先級不同的關(guān)鍵字

比如說成績的排序,如果兩個人總分相同,則語文高的排在前面,語文成績也相同則數(shù)學(xué)高的排在前面。如果對數(shù)字進(jìn)行排序,那么個位、十位、百位就是不同優(yōu)先級的關(guān)鍵字,如果要進(jìn)行升序排序,那么個位、十位、百位優(yōu)先級一次增加。

基數(shù)排序是通過多次的收分配和收集來實(shí)現(xiàn)的,關(guān)鍵字優(yōu)先級低的先進(jìn)行分配和收集。

public static void main(String[] args) {int[] arr = {5, 3, 4, 8, 2};radixSort(arr);System.out.println(Arrays.toString(arr)); }/*** 基數(shù)排序:穩(wěn)定。* 時間復(fù)雜度:最好為 O(d(n+r)),最差為 O(d(n+r)),平均為 O(d(n+r))。空間復(fù)雜度:O(r)* d為位數(shù),r為基數(shù)(較復(fù)雜)* @param arr 數(shù)組*/ public static void radixSort(int[] arr) {if (arr == null || arr.length == 0) {return;}int maxBit = getMaxBit(arr);for (int i = 1; i <= maxBit; i++) {List<List<Integer>> buf = distribute(arr, i); // 分配collecte(arr, buf); // 收集} }/*** 分配* @param arr 待分配數(shù)組* @param iBit 要分配第幾位* @return*/ public static List<List<Integer>> distribute(int[] arr, int iBit) {List<List<Integer>> buf = new ArrayList<>();for (int i = 0; i < 10; i++) {buf.add(new LinkedList<>());}for (int i = 0; i < arr.length; i++) {buf.get(getNBit(arr[i], iBit)).add(arr[i]);}return buf; }/*** 收集* @param arr 把分配的數(shù)據(jù)收集到arr中* @param buf*/ public static void collecte(int[] arr, List<List<Integer>> buf) {int k = 0;for (List<Integer> bucket : buf) {for (int ele : bucket) {arr[k++] = ele;}} }/*** 獲取最大位數(shù)* @param arr* @return*/ public static int getMaxBit(int[] arr) {int max = Integer.MIN_VALUE;for (int ele : arr) {int len = (ele + "").length();if (len > max) {max = len;}}return max; }/*** 獲取x的第n位,如果沒有則為0* @param x* @param n* @return*/ public static int getNBit(int x, int n){String sx = x + "";if (sx.length() < n) {return 0;} else {return sx.charAt(sx.length() - n) - '0';} }

桶排序

桶排序算是計數(shù)排序的一種改進(jìn)和推廣,但是網(wǎng)上有許多資料把計數(shù)排序和桶排序混為一談。其實(shí)桶排序要比計數(shù)排序復(fù)雜許多。
對桶排序的分析和解釋借鑒這位兄弟的文章(有改動):http://hxraid.iteye.com/blog/647759。
桶排序的平均時間復(fù)雜度為線性的O(N+C),其中C=N*(logN-logM)。如果相對于同樣的N,桶數(shù)量M越大,其效率越高,最好的時間復(fù)雜度達(dá)到O(N)。 當(dāng)然桶排序的空間復(fù)雜度 為O(N+M),如果輸入數(shù)據(jù)非常龐大,而桶的數(shù)量也非常多,則空間代價無疑是昂貴的。此外,桶排序是穩(wěn)定的。

public static void main(String[] args) {int[] arr = {5, 3, 4, 8, 2};bucketSort(arr);System.out.println(Arrays.toString(arr)); }/*** 桶排序:穩(wěn)定。將數(shù)組用桶來存儲,關(guān)鍵在于映射函數(shù),對每個桶可以使用快速排序,* 然后每個桶的元素比下一個桶的最小元素小。** 時間復(fù)雜度:最好為 O(n),最差為 O(nlogn),平均為 O(n+c)。空間復(fù)雜度:O(n+m)* n為數(shù)的個數(shù),m為桶的個數(shù),c=n*(logn-logm)。桶越多,效率越高,n=m 達(dá)到 O(n),但是占用很大的* 空間,桶內(nèi)可用快排等* @param arr 數(shù)組*/ public static void bucketSort(int[] arr) {if (arr == null || arr.length == 0) {return;}// int bucketNums = 10; // 這里默認(rèn)為10,規(guī)定待排數(shù)[0,100]List<List<Integer>> lists = new ArrayList<>(); // 桶保存數(shù)據(jù)for (int i = 0; i < 10; i++) {lists.add(new LinkedList<>()); // 鏈表結(jié)構(gòu)}// 將數(shù)組中的數(shù)據(jù)添加到桶中for (int i = 0; i < arr.length; i++) {lists.get(f(arr[i])).add(arr[i]);}// 將每個桶的元素進(jìn)行排序for (int i = 0; i < lists.size(); i++) {Collections.sort(lists.get(i));}// 得到排序后的數(shù)組int k = 0;for (int i = 0; i < lists.size(); i++) {for (int val: lists.get(i)) {arr[k++] = val;}} }/*** hash函數(shù),桶的散列函數(shù)* @param x 輸入一個整數(shù)* @return 散列函數(shù)值*/ public static int f(int x) {return x / 10; }

總結(jié)

以上是生活随笔為你收集整理的排序算法 - 面试中的排序算法总结的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。