几种常见的排序算法总结
常見的幾種排序算法
排序算法有很多,比較常見的有:冒泡排序、選擇排序、插入排序、希爾排序、歸并排序、快速排序、堆排序、計數(shù)排序、桶排序、基數(shù)排序等。并不是所有的都需要會。
本文只會對其中部分算法進行總結(jié)。
冒泡排序
冒泡排序是一種比較簡單的排序方法。也比較好理解,但是通常情況下性能不是很好。在冒泡排序中,序列中的每個數(shù)據(jù)就是水中的泡泡一樣,一個個的向上冒出來,直到冒出水面(達到最大位置)。
(PS:此處說的是從小到大排序,而從大到小排列只需要換個思路)
算法步驟
1、從開頭到結(jié)尾遍歷數(shù)組,比較相鄰的元素。如果前一個比后一個大,就交換他們兩個。
point
|
nums = [4,35,23,34,5,4]
// point 此時發(fā)現(xiàn) nums[point] 比 nums[point + 1] 小,調(diào)換他倆的位置。
2、對每一個相鄰的數(shù)據(jù)進行對比,直到序列結(jié)尾的最后一對,此時“最大值”已經(jīng)被移動到了“最后一個位置”。
point
|
nums = [4,23,34,5,35,4]
// 當 point 到達倒數(shù)第二個位置,此時發(fā)現(xiàn) nums[point] 比 nums[point + 1]小
// 調(diào)換她倆位置后,就把 35 放到了最后一個,此時最大值已經(jīng)找出。
3、重復 1和2 操作。但是每次做完 1和2 操縱后,需要遍歷的數(shù)就少一個(最后一個),因為每次都會有一個最大值已經(jīng)被排好了放到了最后。
實現(xiàn)
Java 實現(xiàn)
public class BubbleSort {
public static void main(String[] args) {
int[] nums = {12,123,432,23,1,3,6,3,-1,6,2,6};;
sort(nums);
System.out.printf("finish !");
}
public static void sort(int[] nums){
int temp ;
for(int len = nums.length ; len > 0; len --){
// 第一層遍歷 len 是需要排序的數(shù)組長度。
for(int i = 0 ; i < len - 1 ; i++){
// 第二層遍歷,遍歷的數(shù)據(jù),每次都少一。
// 但是每次都會把一個最大值放到最后 nums[len - 1] 的位置。
if(nums[i] > nums[i + 1]){
temp = nums[i];
nums[i] = nums[i + 1];
nums[i + 1] = temp;
}
}
}
}
}
選擇排序
選擇排序是一種直觀的排序方法。他和冒泡排序一樣,需要多次遍歷序列。不過冒泡排序,是將最大值挨個的替換相鄰數(shù)據(jù)(冒泡)的方式最后放到最大值的位置的。而選擇排序,通過一個指針(point),標記了最大值所在的索引位置。當遍歷到最后的時候,將標記的最大值所在的位置與最后一個數(shù)交換。
算法步驟
1、從頭到尾的遍歷數(shù)列,遍歷過程中,用一個指針記錄最大值(最小值)所在的位置。
2、將最大值所在位置的數(shù)據(jù)與最后一個交換。
3、重復 1和2 步,每次重復后,需要遍歷的數(shù)列長度就減 1。
實現(xiàn)
Java 版
public class SelectionSort {
public static void main(String[] args) {
int[] nums = {12,123,432,23,1,3,6,3,-1,6,2,6};;
sort(nums);
System.out.printf("finish !");
}
public static void sort(int[] nums){
int max ; // 最大數(shù)所在的位置。
int temp;
for(int len = nums.length ; len > 0; ){
max = 0;
for(int i = 0 ; i < len ; i++){
if(nums[i] > nums[max]){
max = i;
}
}
temp = nums[max];
nums[max]= nums[len - 1];
nums[ --len] = temp;
}
}
}
我們發(fā)現(xiàn),選擇排序每次找最大值的時候,都要遍歷剩下的所有元素。我們有什么方法可以優(yōu)化每次查找最大(最?。┲档乃俣饶??后面會講到的“堆排序”就是為了優(yōu)化查找最大值的。
插入排序
插入排序的思想是將一個待排序的元素,插入到一個已經(jīng)排序好的元素的指定位置。比如我們打撲克牌的時候,每次拿到一張牌,我們會將他插入到手中已經(jīng)排好順序的手牌中,這樣當我們拿到所有的撲克牌后,手中自然就有序了。
對比到到具體的編程中,我們可以用一個指針將一個序列分割成左右兩部分,左邊認為是已排序號(手中的牌),右邊每次取一個放到左邊的序列中。
算法步驟
有如下數(shù)組:[9,3,4,2]
1、用一個指針 i ,指向數(shù)組的 1 的位置。此指針將數(shù)組分為左右兩邊 [9] 和 [3,4,2]。此時左邊只有一個數(shù),所以是有序的,右邊是無序的。
i
|
[9,3,4,2]
2、將 3 依次與前面有序的數(shù)對比,如果比前面的數(shù)小,就將兩個位置上的數(shù)交換直到把 i 位置的數(shù)放到正確的位置。
i
|
[9,3,4,2]
此時nums[i] < nums[i-1],交換兩個數(shù)。
[3,9,4,2]
3、將 i 向后移一位,然后重復 2操作。
i
|
[3,9,4,2]
實現(xiàn)
Java 版
public class InsertSort {
public static void main(String[] args) {
int[] nums = {5,1,4,6,2,66,-1,34,-9,8};
sort(nums);
System.out.println("finish!");
}
public static void sort(int[] nums){
for(int i = 1 ; i < nums.length ; i++){
for(int j = i ; j > 0; j--){
if(nums[j] < nums[j - 1]){
swap(nums,j,j - 1);
}else{
break;
}
}
}
}
public static void swap(int[] nums,int i,int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
插入排序有一個優(yōu)化版本“希爾排序”,本文中就不詳細講了,感興趣的可以去搜一下。
歸并排序
要將一個數(shù)組排序,可以先將它分成兩半分別排序,然后再將結(jié)果合并(歸并)起來。這里的分成的兩半,每部分可以使用其他排序算法,也可以仍然使用歸并排序(遞歸)。
我看《算法》這本書里對歸并算法有兩種實現(xiàn),一種是“自頂向下”,另一種是“自底向上”。這兩種方法,個人認為只是前者用了遞歸的方法,后者使用兩個 for 循環(huán)模擬了遞歸壓棧出棧的算法,本質(zhì)上還是相同的(如果理解錯誤,還望大佬指出)。
算法步驟
1、將要排序的序列分成兩部分。
2、將兩部分分別各自排序。然后再將兩個已經(jīng)排序好的序列“歸并”到一起,歸并后的整個序列就是有序的。
3、將兩個有序的序列歸并的步驟:
3.1、先申請一個空間,大小足夠容納兩個已經(jīng)排序的序列。
3.2、設定兩個指針,最初位置分別為兩個已經(jīng)排序序列的起始位置。
3.3、比較兩個指針所指向的元素,選擇相對小的元素放入到合并空間,并移動指針到下一位置。
3.4、重復3.3 步驟。
歸并排序,比較重要的是“分治”思想和“歸并”的操作。
歸并操作,是將兩個“有序”的序列,合并成一個有序的序列的方法。而這兩個有序的序列,又是根據(jù)“分治”思想將一個學列分割成的兩部分(將一個序列不斷的分隔,到最后就剩一個的時候他自然就是有序的)。
實現(xiàn)
Java 版
public class MergeSort {
public static void main(String[] args) {
int[] nums = {12,123,432,23,1,3,6,3,-1,6,2,6};;
sort(nums,0,nums.length -1);
System.out.printf("finish!");
}
public static void sort(int[] nums,int left,int right){
if(left >= right){
return;
}
// 遞歸的將左半邊排序
sort(nums,left,right - left / 2 - 1);
// 遞歸的將右半邊排序
sort(nums,right - left / 2 ,right);
// 此時左右半邊都分別各自有序,再將其歸并到一起。
// 如:[1,9,10 , 3,7,8]
merge(nums,left,right - left / 2,right);
}
// 此方法叫做原地歸并,將數(shù)組 nums 根據(jù) mid 分隔開,左右看作是兩個數(shù)組。
// 類似于 merge(int[] nums1,int[] nums2),將 nums1 和 nums2 歸并
public static void merge(int[] nums ,int left,int mid,int right){
int i = left,j = mid;
int[] temp_nums = new int[nums.length];
for(int key = left ; key <= right; key++)
// 將原來數(shù)組復制到臨時數(shù)組中。
temp_nums[key] = nums[key];
for(int key = i ; key <= right; key++){
if(i > mid){
nums[key] = temp_nums[j++];
} else if (j > right) {
nums[key] = temp_nums[i++];
} else if (temp_nums[i] > temp_nums[j]) {
nums[key] = temp_nums[j++];
}else{
nums[key] = temp_nums[i++];
}
}
}
}
快速排序
快速排序是一種分治的排序算法,它將一個數(shù)組分成兩個子數(shù)組,將兩部分獨立的排序
快速排序可能是應用最廣泛的排序算法了。快速排序流行的原因是它實現(xiàn)簡單、適用于各種不同的輸入數(shù)據(jù)且在一般應用中比其他排序算法都要快得多。——《算法(第四版)》
算法步驟
- 從數(shù)列中挑出一個元素,稱為 "基準"(pivot);
- 所有元素比"基準"值小的擺放在前面,所有元素比"基準"值大的擺在后面,相同的數(shù)可以到任一邊。這個稱為分區(qū)(partition)操作。
- 遞歸地(recursive)使用同樣的方法把小于基準值元素的子數(shù)列和大于基準值元素的子數(shù)列排序;
算法過程
1、給定一個亂序的數(shù)組
[5,1,4,6,2,66,34,8]
2、選擇第一個為基準數(shù),此時把第一個位置置空。兩個指針,left從左到右,找比 piovt “大”的數(shù);right 從右向左,找比 piovt “小”的數(shù)。
left right
| |
[_,1,4,6,2,66,34,8]
|
piovt = 5
3、right 從右向左(<-),找比 piovt “小”的數(shù) 2。
left right
| |
[_,1,4,6,2,66,34,8]
|
piovt = 5
4、left從左到右(->),找到了比 piovt 大的數(shù) 6。
left right
| |
[_,1,4,6,2,66,34,8]
|
piovt = 5
5、此時將 left 和 right 上的數(shù)對調(diào)。
left right
| |
[_,1,4,2,6,66,34,8]
|
piovt = 5
6、right 繼續(xù)向左查找,直到 left = right。(正常情況下要重復 4、5 步驟多次才會得到 left = right)
此時將 left 位置的數(shù)放到原來 piovt 位置上,將 piovt 放到 left 位置上。
left
right
|
[2,1,4,5,6,66,34,8]
- -
|
piovt = 5
7、此時將整個數(shù)組根據(jù) piovt 分割成兩個部分,左邊都比 piovt 小,右邊都比 piovt 大。遞歸的處理左右兩部分。
實現(xiàn)
Java 版
public class QuickSort {
public static void main(String[] args) {
int[] nums = {5,1,4,6,2,66,34,8,34,534,5};
int[] sorted = sort(nums,0 , nums.length - 1);
System.out.println("finish!");
}
// 排序
public static int[] sort(int[] nums , int left , int right){
if(left <= right){
// 將 nums 以 mid 分成兩部分
// 左邊的小于 nums[min]
// 右邊的大于 nums[min]
int mid = partition(nums,left,right);
// 遞歸
sort(nums,left,mid - 1);
sort(nums,mid + 1 ,right);
}
return nums;
}
public static int partition(int[] nums , int left , int right){
//int pivot = left;
int i = left , j = right + 1; // 左右兩個指針
int pivot = nums[left]; // 基準數(shù),比他小的放到左邊,比他大的放到右邊。
while ( true ){
// 從右向左找比 pivot 小的。
while (j > left && nums[--j] > pivot){
if(j == left){
// 到頭了
break;
}
}
// 先從左向右找比 pivot 大的。
while (i < right && nums[ ++ i] < pivot ){
if( i == right){
// 到頭了
break;
}
}
if(i >= j ) break;
// 交換 i 位置和 j 位置上的數(shù)
// 因為此時 nums[i] > pivot 并且 nums[j] < pivot
swap(nums,i , j);
}
// 由于 left 位置上的數(shù)是 pivot=
// 此時 i = j 且 nums[i/j] 左邊的數(shù)都小于 pivot , nums[i/j] 右邊的數(shù)都大于 pivot。
// 此時交換 left 和 j 位置上的數(shù)就是將 pivot 放到中間
swap(nums,left,j);
return j ;
}
// 交換數(shù)組中兩個位置上的數(shù)
public static void swap(int[] nums , int i1 , int i2){
int n = nums[i1];
nums[i1] = nums[i2];
nums[i2] = n;
}
}
堆排序
堆排序主要是利用“堆”這種數(shù)據(jù)結(jié)構(gòu)的特性來進行排序,它本質(zhì)上類似于“選擇排序”。都是每次將最大值(或最小值),找出來放到數(shù)列尾部。不過“選擇排序”需要遍歷整個數(shù)列后選出最大值(可以到上面再熟悉下選擇排序算法),“堆排序”是依靠堆這種數(shù)據(jù)結(jié)構(gòu)來選出最大值。但是每次重新構(gòu)建最大堆用時要比遍歷整個數(shù)列要快得多。
堆排序中用到的兩種堆,大頂堆和小頂堆:
1、大頂堆:每個節(jié)點的值都大于或等于其子節(jié)點的值(在堆排序算法中一般用于升序排列);
2、小頂堆:每個節(jié)點的值都小于或等于其子節(jié)點的值(在堆排序算法中一般用于降序排列);
我們給樹的每個節(jié)點編號,并將編號映射到數(shù)組的下標就是這樣:
該數(shù)組從邏輯上是一個堆結(jié)構(gòu),我們用公式來描述一下堆的定義就是:
1、大頂堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
2、小頂堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
這里只要求父節(jié)點大于兩個子節(jié)點,并沒有要求左右兩個子節(jié)點的大小關(guān)系。
算法過程
1、將一個 n 長的待排序序列arr = [0,……,n-1]構(gòu)造成一個大頂堆。
2、此時數(shù)組的 0 位置(也就是堆頂),就是數(shù)組的最大值了,將其與數(shù)組的最后一個數(shù)交換。
3、將剩下 n-1 個數(shù)重復 1和2 操作,最終會得到一個有序的序列。
堆排序是“選擇排序”的一種變體,算法中比較難的地方是用數(shù)組構(gòu)建“大頂堆”或“小頂堆”的過程。
實現(xiàn)堆排序前,我們要知道怎么用數(shù)組構(gòu)建一個邏輯上的最大堆,這里會用到幾個公式(假設當前節(jié)點的序號是 i,可以結(jié)合上圖理解下下面的公式):
1、左子節(jié)點的序號就是:2i + 1;
2、右子幾點的序號就是:2i + 2;
3、父節(jié)點的序號就是:(i-1) / 2 (i不為0);
實現(xiàn)
Java 版
public class HeapSort {
static int temp ;
public static void main(String[] args) {
int[] nums = {5,1,4,6,2,66,-1,34,-9,8};
sort(nums);
System.out.println("finish!");
}
public static void sort(int[] nums){
// 第一步要先將 nums 構(gòu)建成最大堆。
for(int i = (nums.length - 1) / 2 ; i >= 0; i-- ){
//從第一個非葉子結(jié)點從下至上,從右至左調(diào)整結(jié)構(gòu)
maxHeapify(nums,i,nums.length);
}
// 遍歷數(shù)組
// j 是需要排序的數(shù)組的最后一個索引位置。
for(int j = nums.length - 1 ; j > 0 ; j --){
// 每次都調(diào)整最大堆堆頂(nums[0]),與數(shù)組尾的數(shù)據(jù)位置(nums[j])。
swap(nums,0,j);
// 重新調(diào)整最大堆
maxHeapify(nums,0,j);
}
}
/**
* 將 nums 從 i 開始的 len 長度調(diào)整成最大堆。
* (注意:此方法只適合調(diào)整已經(jīng)是最大堆但是被修改了的堆,或者只有三個節(jié)點的堆)
* len :需要計算到數(shù)組 nums 的多長的地方。
* i :父節(jié)點在的位置。
*/
public static void maxHeapify(int[] nums,int i , int len){
// 是從左子節(jié)點開始
int key = 2 * i + 1;
if(key >= len){
// 說明沒有子節(jié)點。
return;
}
// key + 1 是右子節(jié)點的位置。
if(key + 1 < len && nums[key] < nums[key + 1]){
// 此時說明右節(jié)點比左節(jié)點大。
// 此時只要將父節(jié)點跟 右子節(jié) 點比就行了。
key += 1;
}
if(nums[i] < nums[key]){
// 子節(jié)點比父節(jié)點大,交換子父界節(jié)點的數(shù)據(jù),將父節(jié)點設置為最大。
swap(nums,i,key);
// 此時子節(jié)點上的數(shù)變了,就要遞歸的再去,計算子節(jié)點是不是最大堆。
maxHeapify(nums,key,len);
}
}
/**
* 交換 i 和 j 位置的數(shù)據(jù)
*/
public static void swap(int[] nums,int i,int j){
temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
maxHeapify 這個方法有很多種實現(xiàn),這里用了個比較容易理解的遞歸實現(xiàn)。我看 dreamcatcher-cx 大佬寫了一種更好的實現(xiàn)方法,比較難理解一點,但是更高效,感興趣的見【參考4】。
參考
1、算法(第四版),by Robert Sedgewick/Kevin Wayne。
2、十大經(jīng)典排序算法,by runnoob.com。
3、神級基礎排序——快速排序,by 江神。
4、圖解排序算法(三)之堆排序,by dreamcatcher-cx。
總結(jié)
以上是生活随笔為你收集整理的几种常见的排序算法总结的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 华为Mate 40 Pro最新渲染图曝光
- 下一篇: NOIP2023 游寄