JAVA實現八大排序+二分查詢
JAVA實現八大排序+二分查詢
簡單介紹
排序是計算機內經常進行的一種操作,其目的是將一組“無序”的記錄序列調整為“有序”的記錄序列。
排序分為內部排序和外部排序。
若整個排序過程不需要訪問外存便能完成,則稱此類排序問題為內部排序。
反之,若參加排序的記錄數量很大,整個序列的排序過程不可能在記憶體中完成,則稱此類排序問題為外部排序。
八大排序演算法均屬於內部排序。如果按照策略來分類,大致可分為:交換排序、插入排序、選擇排序、歸併排序和基數排序。如下圖所示:
下表給出各種排序的基本效能,具體分析請參看各排序的詳解:
直接插入排序
基本思想
通常人們整理橋牌的方法是一張一張的來,將每一張牌插入到其他已經有序的牌中的適當位置。在計算機的實現中,為了要給插入的元素騰出空間,我們需要將其餘所有元素在插入之前都向右移動一位。
演算法描述
一般來說,插入排序都採用in-place在陣列上實現。具體演算法描述如下:
- 從第一個元素開始,該元素可以認為已經被排序
- 取出下一個元素,在已經排序的元素序列中從後向前掃描
- 如果該元素(已排序)大於新元素,將該元素移到下一位置
- 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置
- 將新元素插入到該位置後
- 重複步驟2~5
平均時間複雜度 | 最好情況 | 最壞情況 | 空間複雜度 |
---|---|---|---|
O(n²) | O(n²) | O(n²) | O(1) |
插入排序所需的時間取決於輸入元素的初始順序。例如,對一個很大且其中的元素已經有序(或接近有序)的陣列進行排序將會比隨機順序的陣列或是逆序陣列進行排序要快得多。
如果 比較操作 的代價比 交換操作 大的話,可以採用二分查詢法來減少 比較操作 的數目。該演算法可以認為是 插入排序 的一個變種,稱為二分查詢插入排序。
Java實現
private static void 直接排序(int[] array){ for (int compare_position = 0; compare_position < array.length - 1; compare_position++) { for (int compared_position = compare_position + 1; compared_position > 0; compared_position--) { //交換值 if (array[compared_position] < array[compared_position - 1]) { int temp = array[compared_position]; array[compared_position] = array[compared_position - 1]; array[compared_position - 1] = temp; } } } }
氣泡排序
基本思想
氣泡排序(Bubble Sort)是一種簡單的排序演算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個演算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端。由於氣泡排序只在相鄰元素大小不符合要求時才調換他們的位置, 它並不改變相同元素之間的相對順序, 因此它是穩定的排序演算法。氣泡排序是最容易實現的排序, 最壞的情況是每次都需要交換, 共需遍歷並交換將近n²/2次, 時間複雜度為O(n²). 最佳的情況是內迴圈遍歷一次後發現排序是對的, 因此退出迴圈, 時間複雜度為O(n). 平均來講, 時間複雜度為O(n²). 由於氣泡排序中只有快取的temp變數需要記憶體空間, 因此空間複雜度為常量O(1).
演算法描述
氣泡排序演算法的運作如下:
- 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
- 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。
- 針對所有的元素重複以上的步驟,除了最後一個。
- 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
平均時間複雜度 | 最好情況 | 最壞情況 | 空間複雜度 |
---|---|---|---|
O(n²) | O(n) | O(n²) | O(1) |
Java實現
private static void 氣泡排序(int[] array){
for (int compare_times = 0 ; compare_times < array.length-1; compare_times++) //外層迴圈控制比較的次數
{
for (int position = 0 ; position < array.length-compare_times-1; position++ ) ////內層迴圈控制到達位置
{
//前面的元素比後面大就交換
if (array[position]>array[position+1]){
int temp = array[position];
array[position]=array[position+1];
array[position+1]=temp;
}
}
}
}
快速排序
基本思想
快速排序是由東尼·霍爾所發展的一種排序演算法。在平均狀況下,排序 n 個專案要 Ο(nlogn) 次比較。在最壞狀況下則需要 Ο(n2) 次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他 Ο(nlogn) 演算法更快,因為它的內部迴圈(inner loop)可以在大部分的架構上很有效率地被實現出來。
快速排序的基本思想:挖坑填數+分治法。
快速排序使用分治法(Divide and conquer)策略來把一個序列(list)分為兩個子序列(sub-lists)。
快速排序又是一種分而治之思想在排序演算法上的典型應用。本質上來看,快速排序應該算是在氣泡排序基礎上的遞迴分治法。
快速排序的名字起的是簡單粗暴,因為一聽到這個名字你就知道它存在的意義,就是快,而且效率高!它是處理大資料最快的排序演算法之一了。雖然 Worst Case 的時間複雜度達到了 O(n²),但是人家就是優秀,在大多數情況下都比平均時間複雜度為 O(n logn) 的排序演算法表現要更好。
演算法描述
快速排序使用分治策略來把一個序列(list)分為兩個子序列(sub-lists)。步驟為:
- 從數列中挑出一個元素,稱為"基準"(pivot)。
- 重新排序數列,所有比基準值小的元素擺放在基準前面,所有比基準值大的元素擺在基準後面(相同的數可以到任一邊)。在這個分割槽結束之後,該基準就處於數列的中間位置。這個稱為分割槽(partition)操作。
- 遞迴地(recursively)把小於基準值元素的子數列和大於基準值元素的子數列排序。
遞迴到最底部時,數列的大小是零或一,也就是已經排序好了。這個演算法一定會結束,因為在每次的迭代(iteration)中,它至少會把一個元素擺到它最後的位置去。
平均時間複雜度 | 最好情況 | 最壞情況 | 空間複雜度 |
---|---|---|---|
O(nlog₂n) | O(nlog₂n) | O(n²) | O(1)(原地分割槽遞迴版) |
Java實現
private static void 快速排序_遞迴(int[] array,int low,int high){
if (low>high){
return;
}
int left_limit = low,right_limit = high,compare = array[left_limit];//基值
while(left_limit<right_limit){
//從後向前找到比基準小的元素
while(left_limit<right_limit&&array[right_limit]>=compare){
right_limit--;
}
array[left_limit] = array[right_limit];
//從前往後找到比基準大的元素
while (left_limit<right_limit&&array[left_limit]<=compare){
left_limit++;
}
array[right_limit] = array[left_limit];
}
array[left_limit]=compare;
快速排序_遞迴(array,low,left_limit-1);
快速排序_遞迴(array,left_limit+1,high);
}
簡單選擇排序
基本思想
選擇排序(Selection sort)是一種簡單直觀的排序演算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
選擇排序的主要優點與資料移動有關。如果某個元素位於正確的最終位置上,則它不會被移動。選擇排序每次交換一對元素,它們當中至少有一個將被移到其最終位置上,因此對 n個元素的表進行排序總共進行至多 n-1 次交換。在所有的完全依靠交換去移動元素的排序方法中,選擇排序屬於非常好的一種。
選擇排序的簡單和直觀名副其實,這也造就了它”出了名的慢性子”,無論是哪種情況,哪怕原陣列已排序完成,它也將花費將近n²/2次遍歷來確認一遍。即便是這樣,它的排序結果也還是不穩定的。 唯一值得高興的是,它並不耗費額外的記憶體空間。
演算法描述
- 從未排序序列中,找到關鍵字最小的元素
- 如果最小元素不是未排序序列的第一個元素,將其和未排序序列第一個元素互換
- 重複1、2步,直到排序結束。
動圖效果如下所示:
平均時間複雜度 | 最好情況 | 最壞情況 | 空間複雜度 |
---|---|---|---|
O(n²) | O(n²) | O(n²) | O(1) |
Java實現
private static void 簡單選擇排序(int[] array){
for (int i = 0; i < array.length; i++) {
int min = i;
for (int j = i+1; j < array.length ; j++ ){
if (array[j]<array[min]){
min = j;
}
}
if (min!=i){
int temp = array[i];
array[i] = array[min];
array[min] = temp;
}
}
}
希爾排序
基本思想
希爾排序,也稱 遞減增量排序演算法,是插入排序的一種更高效的改進版本。希爾排序是 非穩定排序演算法。
希爾排序是基於插入排序的以下兩點性質而提出改進方法的:
- 插入排序在對幾乎已經排好序的資料操作時,效率高,即可以達到線性排序的效率
- 但插入排序一般來說是低效的,因為插入排序每次只能將資料移動一
希爾排序是先將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行依次直接插入排序。
將待排序陣列按照步長gap進行分組,然後將每組的元素利用直接插入排序的方法進行排序;每次再將gap折半減小,迴圈上述操作;當gap=1時,利用直接插入,完成排序。
可以看到步長的選擇是希爾排序的重要部分。只要最終步長為1任何步長序列都可以工作。一般來說最簡單的步長取值是初次取陣列長度的一半為增量,之後每次再減半,直到增量為1。更好的步長序列取值可以參考維基百科。
希爾排序更高效的原因是它權衡了子陣列的規模和有序性。排序之初,各個子陣列都很短,排序之後子陣列都是部分有序的,這兩種情況都很適合插入排序。
演算法描述
- 選擇一個增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
- 按增量序列個數 k,對序列進行 k 趟排序;
- 每趟排序,根據對應的增量 ti,將待排序列分割成若干長度為 m 的子序列,分別對各子表進行直接插入排序。僅增量因子為 1 時,整個序列作為一個表來處理,表長度即為整個序列的長度。
平均時間複雜度 | 最好情況 | 最壞情況 | 空間複雜度 |
---|---|---|---|
O(nlog2 n) | O(nlog2 n) | O(nlog2 n) | O(1) |
Java實現
private static void 希爾排序(int[] array){
int length = array.length;
int h = 1;
while (h < length / 3)
h = 3 * h + 1;
for (; h >= 1; h /= 3) {
for (int i = 0; i < array.length - h; i += h) {
for (int j = i + h; j > 0; j -= h) {
if (array[j] < array[j - h]) {
int temp = array[j];
array[j] = array[j - h];
array[j - h] = temp;
}
}
}
}
}
歸併排序
基本思想
歸併排序是建立在歸併操作上的一種有效的排序演算法,1945年由約翰·馮·諾伊曼首次提出。該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用,且各層分治遞迴可以同時進行。
歸併排序演算法是將兩個(或兩個以上)有序表合併成一個新的有序表,即把待排序序列分為若干個子序列,每個子序列是有序的。然後再把有序子序列合併為整體有序序列。
從效率上看,歸併排序可算是排序演算法中的”佼佼者”. 假設陣列長度為n,那麼拆分陣列共需logn,, 又每步都是一個普通的合併子陣列的過程, 時間複雜度為O(n), 故其綜合時間複雜度為O(nlogn)。另一方面, 歸併排序多次遞迴過程中拆分的子陣列需要儲存在記憶體空間, 其空間複雜度為O(n)。
演算法描述
遞迴法(假設序列共有n個元素):
- 將序列每相鄰兩個數字進行歸併操作,形成 floor(n/2)個序列,排序後每個序列包含兩個元素;
- 將上述序列再次歸併,形成 floor(n/4)個序列,每個序列包含四個元素;
- 重複步驟2,直到所有元素排序完畢。
迭代法
- 申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合併後的序列
- 設定兩個指標,最初位置分別為兩個已經排序序列的起始位置
- 比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置
- 重複步驟3直到某一指標到達序列尾
- 將另一序列剩下的所有元素直接複製到合併序列尾
平均時間複雜度 | 最好情況 | 最壞情況 | 空間複雜度 |
---|---|---|---|
O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(n) |
Java實現
private static int[] aux;//歸併所需陣列
private static void merge(int[] array,int low ,int mid, int high){
//將array[low..mid]和array[mid+1..high]歸併
int i = low,j = mid+1;
for (int k = low; k <= high ; k++ ){
aux[k]=array[k];
}
for (int k = low; k <= high ; k++ ){
if (i>mid) //左邊取盡
{
array[k]=aux[j++];
}else if (j>high)
{
array[k] = aux[i++];
} else if (aux[j] < aux[i]) {
array[k] = aux[j++];
} else {
array[k] = aux[i++];
}
}
}
private static 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);
}
private static void 歸併排序(int[] array){
//分配空間
aux = new int[array.length];
sort(array,0,array.length-1);
}
基數排序
基本思想
基數排序的發明可以追溯到1887年赫爾曼·何樂禮在打孔卡片製表機(Tabulation Machine), 排序器每次只能看到一個列。它是基於元素值的每個位上的字元來排序的。 對於數字而言就是分別基於個位,十位, 百位或千位等等數字來排序。
基數排序(Radix sort)是一種非比較型整數排序演算法,其原理是將整數按位數切割成不同的數字,然後按每個位數分別比較。由於整數也可以表達字串(比如名字或日期)和特定格式的浮點數,所以基數排序也不是隻能使用於整數。
它是這樣實現的:將所有待比較數值(正整數)統一為同樣的數位長度,數位較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後,數列就變成一個有序序列。
基數排序按照優先從高位或低位來排序有兩種實現方案:
- MSD(Most significant digital) 從最左側高位開始進行排序。先按k1排序分組, 同一組中記錄, 關鍵碼k1相等, 再對各組按k2排序分成子組, 之後, 對後面的關鍵碼繼續這樣的排序分組, 直到按最次位關鍵碼kd對各子組排序後. 再將各組連線起來, 便得到一個有序序列。MSD方式適用於位數多的序列。
- LSD (Least significant digital)從最右側低位開始進行排序。先從kd開始排序,再對kd-1進行排序,依次重複,直到對k1排序後便得到一個有序序列。LSD方式適用於位數少的序列。
演算法描述
我們以LSD為例,從最低位開始,具體演算法描述如下:
- 取得陣列中的最大數,並取得位數;
- arr為原始陣列,從最低位開始取每個位組成radix陣列;
- 對radix進行計數排序(利用計數排序適用於小範圍數的特點);
平均時間複雜度 | 最好情況 | 最壞情況 | 空間複雜度 |
---|---|---|---|
O(d*(n+r)) | O(d*(n+r)) | O(d*(n+r)) | O(n+r) |
其中,d 為位數,r 為基數,n 為原陣列個數。在基數排序中,因為沒有比較操作,所以在複雜上,最好的情況與最壞的情況在時間上是一致的,均為 O(d*(n + r))
。
Java實現
private static void 基數排序(int[] array){
if (array.length<=1){
return;
}
//取得陣列中的最大數,並取得位數
int max = 0;
for (int i = 0; i < array.length; i++) {
if (max < array[i]) {
max = array[i];
}
}
//通過maxDigit記錄陣列中最大數的位數
int maxDigit = 1;
while(max/10>0){
maxDigit++;
max = max / 10;
}
//申請一個桶空間
int[][] buckets = new int[10][array.length];
int base = 10;
//從低位到高位,對每一位遍歷,將所有元素分配到桶中
for (int i = 0; i < maxDigit; i++) {
int[] bktLen = new int[10]; //儲存各個桶中儲存元素的數量
//分配:將所有元素分配到桶中
for (int j = 0; j < array.length; j++) {
int whichBucket = (array[j] % base) / (base / 10);
buckets[whichBucket][bktLen[whichBucket]] = array[j];
bktLen[whichBucket]++;
}
//收集:將不同桶裡資料挨個撈出來,為下一輪高位排序做準備,由於靠近桶底的元素排名靠前,因此從桶底先撈
int k = 0;
for (int b = 0; b < buckets.length; b++) {
for (int p = 0; p < bktLen[b]; p++) {
array[k++] = buckets[b][p];
}
}
base *= 10;
}
}
堆排序
堆的定義如下:n
個元素的序列{k1,k2,..,kn}
當且僅當滿足下關係時,稱之為堆。
把此序列對應的二維陣列看成一個完全二叉樹。那麼堆的含義就是:完全二叉樹中任何一個非葉子節點的值均不大於(或不小於)其左,右孩子節點的值。 由上述性質可知大頂堆的堆頂的關鍵字肯定是所有關鍵字中最大的,小頂堆的堆頂的關鍵字是所有關鍵字中最小的。因此我們可使用大頂堆進行升序排序, 使用小頂堆進行降序排序。
基本思想
此處以大頂堆為例,堆排序的過程就是將待排序的序列構造成一個堆,選出堆中最大的移走,再把剩餘的元素調整成堆,找出最大的再移走,重複直至有序。
由於堆排序中初始化堆的過程比較次數較多, 因此它不太適用於小序列。 同時由於多次任意下標相互交換位置, 相同元素之間原本相對的順序被破壞了, 因此, 它是不穩定的排序。
演算法描述
- 建立堆的過程, 從length/2 一直處理到0, 時間複雜度為O(n);
- 調整堆的過程是沿著堆的父子節點進行調整, 執行次數為堆的深度, 時間複雜度為O(lgn);
- 堆排序的過程由n次第2步完成, 時間複雜度為O(nlgn).
Java實現
關於樹的公式複習:
- 非空二叉樹葉子結點數 = 度為2的結點數 + 1 即,N0=N2+1
- 非空二叉樹上第K層至多有2k−1 個結點(K≥1)
- 高度為H的二叉樹至多有2H−1 個結點(H≥1)
- 具有N個(N>0)結點的完全二叉樹的高度為 ⌈log2(N+1)⌉ 或 ⌊log2N⌋+1
- 對完全二叉樹按從上到下、從左到右的順序依次編號1,2,...,N,則有以下關係:
- 當 i>1 時,結點 i 的雙親結點編號為 ⌊i/2⌋ ,即當 i 為偶數時,其雙親結點的編號為 i/2 ,它是雙親結點的左孩子;當 i 為奇數時,其雙親結點的編號為 (i−1)/2 ,它是雙親結點的右孩子。
- 當 2i≤N 時,結點i的左孩子編號為 2i ,否則無左孩子。
- 當 2i+1≤N 時,結點i的右孩子編號為 2i+1 ,否則無右孩子。
- 結點 i 所在層次(深度)為 ⌊log2i⌋+1 。(設根結點為第1層)
private static void 堆排序(int[] a){
for (int i = a.length - 1; i > 0; i--) {
max_heapify(a, i);
//堆頂元素(第一個元素)與Kn交換
int temp = a[0];
a[0] = a[i];
a[i] = temp;
}
}
public static void max_heapify(int[] a, int n) {
int child;
for (int i = (n - 1) / 2; i >= 0; i--) {
//左子節點位置
child = 2 * i + 1;
//右子節點存在且大於左子節點,child變成右子節點
if (child != n && a[child] < a[child + 1]) {
child++;
}
//交換父節點與左右子節點中的最大值
if (a[i] < a[child]) {
int temp = a[i];
a[i] = a[child];
a[child] = temp;
}
}
}
做法二:
private static void 堆排序_v2(int[] array){
//將無序序列構成一個堆
for (int i = array.length/2-1 ;i>=0 ; i--){
adjustHeap(array,i,array.length);
}
for (int j = array.length-1;j>0 ; j--){
//交換
int temp = array[j];
array[j] = array[0];
array[0] = temp;
adjustHeap(array,0,j);
}
}
public static void adjustHeap(int[] array,int i,int length){
int temp = array[i]; //取出當前元素的值,儲存在變數中 【父節點】
//start
//k=i*2+1 k是i節點的左子節點
for (int k = i*2+1; k<length; k=k*2+1){//一個是這個一個的左子節點
if (array[k]<array[k+1]&&(k+1)<length){//說明左子節點小於右子節點
k++;
}
if (array[k]>temp){//如果子節點大於父節點
array[i] = array[k]; //把較大的值賦給父節點
i = k; //i指向k,繼續迴圈比較
}else {
break;
}
}
//for結束後,我們已經將以i為父節點的樹的最大值,放在了最頂
array[i] = temp;//將temp值放置到調整後的位置
}
補充:二分查詢
基本思想與演算法描述
二分查詢(binary search),也稱折半搜尋,是一種在 有序陣列 中 查詢某一特定元素 的搜尋演算法。搜尋過程從陣列的中間元素開始,如果中間元素正好是要查詢的元素,則搜尋過程結束;如果某一特定元素大於或者小於中間元素,則在陣列大於或小於中間元素的那一半中查詢,而且跟開始一樣從中間元素開始比較。如果在某一步驟陣列為空,則代表找不到。這種搜尋演算法每一次比較都使搜尋範圍縮小一半。
- 時間複雜度:折半搜尋每次把搜尋區域減少一半,時間複雜度為O(log n)。(n代表集合中元素的個數)
- 空間複雜度: O(1)。雖以遞迴形式定義,但是尾遞迴,可改寫為迴圈。
如何計算二分查詢中的中值?大家一般給出了兩種計算方法:
- 演算法一:
mid = (low + high) / 2
- 演算法二:
mid = low + (high – low)/2
乍看起來,演算法一簡潔,演算法二提取之後,跟演算法一沒有什麼區別。但是實際上,區別是存在的。演算法一的做法,在極端情況下,(low + high)存在著溢位的風險,進而得到錯誤的mid結果,導致程式錯誤。而演算法二能夠保證計算出來的mid,一定大於low,小於high,不存在溢位的問題。
二分查詢法的O(log n)讓它成為十分高效的演算法。不過它的缺陷卻也是那麼明顯的。就在它的限定之上:必須有序,我們很難保證我們的陣列都是有序的。當然可以在構建陣列的時候進行排序,可是又落到了第二個瓶頸上:它必須是陣列。
陣列讀取效率是O(1),可是它的插入和刪除某個元素的效率卻是O(n)。因而導致構建有序陣列變成低效的事情。
解決這些缺陷問題更好的方法應該是使用二叉查詢樹了,最好自然是自平衡二叉查詢樹了,既能高效的(O(n log n))構建有序元素集合,又能如同二分查詢法一樣快速(O(log n))的搜尋目標數。
Java實現
//遞迴實現二分查詢
private static int binary_search_a(int[] array,int left_limit,int right_limit,int target){
if (left_limit>right_limit){
return -1;
}
int mid = left_limit+(right_limit-right_limit)/2;
if (array[mid]>target){
return binary_search_a(array,left_limit,mid-1,target);
}
if (array[mid]<target){
return binary_search_a(array,mid+1,right_limit,target);
}
System.out.println("有查詢到:"+array[mid]+" position: "+(mid+1));
return mid+1;
}
//非遞迴實現二分查詢
private static void binary_search_b(int[] array,int target){
int left_limit = 0,right_limit = array.length-1;
while (left_limit<right_limit){
int mid = left_limit+(right_limit-right_limit)/2;
if (array[mid]>target){
right_limit=mid-1;
}else if (array[mid]<target){
left_limit=mid+1;
}else {
System.out.println("有查詢到:"+array[mid]+" position: "+(mid+1));
return;
}
}
System.out.println("未查詢到: "+target+"!");
return;
}