排序算法
引言
排序算法有好幾種,從最簡單的選擇,冒泡,插入排序,到比較複雜的歸併,堆排序和快速排序,再到計數排序,基數排序和桶排序等。要列舉的話足有上百種,但這裏只列舉幾個常用的排序算法,即冒泡,插入,選擇,歸併和快排。
冒泡排序
冒泡排序是最爲基礎的一種排序方式,也是初學者最爲容易掌握的一種排序方式,下面就簡單介紹一下該算法。
冒泡算法作爲一種O(n^2)的算法,雖然效率上有所欠缺,但卻勝在算法思想簡單易學。該算法的核心思想就是重複遍歷要排序的數列,一次比較數列中的兩個數,如果它們的順序不對,就將他們交換位置,遍歷數列直到沒有元素需要交換爲止,這就和這個算法的名字一樣,大的元素就像泡泡一樣不斷地上浮到最頂端,而小的元素則隨着一次次的交換到最底端。
冒泡排序流程:
1.從第一個開始比較相鄰的兩個元素,如果前一個大於後一個,則交換二者位置;
2.一次遍歷完成後,最大的元素就排在了最後面,接着進行第二次遍歷;
3.第二次遍歷會將前一次遍歷找到的最大值排除,在前面找到前半部分的最大值,將之排在最大值前面,以此類推最後我們就完成了整個排序的過程
圖示:
代碼(java版):
public void Sort(int[] nums) {
for (int i = nums.length - 1; i >= 0; i--) {//最外層循環,控制遍歷的範圍[0~i]
for (int j = 0; j < i; j++) {//在[0~i]的範圍內不斷查找這個範圍內的最大值將之放在最後
if (nums[j] > nums[j + 1]) {//如果前一個數大於後一個數,交換二者位置
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
}
}
選擇排序
選擇排序是一種非常直觀的排序算法,它每一次遍歷都是在未排序部分找到最大(最小)值,然後將它放在未排序的起始位置,然後再從剩餘未排序部分繼續尋找最大(最小)值,直至全部元素排序完成。
選擇排序流程:
1.首先設一個索引,該索引用於保存最小(最大)值的地址,並給之賦一個初值,初值爲未排序的第一個數的索引;
2.接着,從第一個未排序元素開始遍歷,如果該元素小於(大於)所設索引位置的數,則將該元素的索引賦予所設的索引;
3.一次遍歷完成後,將索引位置的值與未排序的第一個值進行交換位置,則已將一個數排好順序;
4.不斷循環以上步驟直至全部元素排好順序;
圖示:
代碼:
public void Sort(int[] nums) {
for (int i = 0; i < nums.length; i++) {//遍歷的次數
int min_index = i;//給min_index賦一個初始值
for (int j = i; j < nums.length; j++) {//從i遍歷到最後一個數,從這裏面找到最小值,將該索引賦給min
if (nums[j] < nums[min_index]) {//如果當前數比index所指向的數還要小,就將當前數的索引賦給index
min_index = j;
}
}
//一次遍歷完成後,將index與i的值交換
int temp = nums[min_index];
nums[min_index] = nums[i];
nums[i] = temp;
}
}
插入排序
插入排序也是一種十分直觀的排序算法,它是通過將要排序的數列分爲已排序部分與未排序部分,然後將未排序的部分不斷插入到前面已排序的數列中,直到全部插入完畢。
插入排序流程:
1.選擇未排序部分的第一個,從已排序的數列中從後向前掃描;
2.如果已排序好的數列的最後一個大於未排序的第一個,則將二者位置交換;
3.接着比較最後第二個是否比之前選擇的數大,如果大,接着交換,否則跳出循環(可以提前終止循環);
圖示:
代碼:
public void Sort(int[] nums) {
for (int i = 1; i < nums.length; i++) {//遍歷的次數
int temp = nums[i];
int j = i;
for (; j > 0 && temp < nums[j - 1]; j--) {//在已排序部分進行查找temp該插入的部分
nums[j] = nums[j - 1];//如果num[j-1]個比temp大,就將前一個值複製給後一個數
}
nums[j] = temp;//此時,j保存的就是temp該放的位置
}
}
歸併排序
歸併排序是一種高效的排序算法,時間複雜度爲nlog(n),該算法的核心思想是分治法,將大問題不斷分解爲小問題直至可以直接求解。
歸併排序流程:
1.將要排序的數列不斷遞歸地分割爲左右兩部分,直至每一部分只有單獨的一個數
2.分割完之後就要進行合併,比較遞歸分割後的左右兩部分,將這兩部分進行比較後,按序放回原始數組中。
圖示:
代碼(遞歸版):
void Sort(int[] nums) {
merge(nums, 0, nums.length - 1);
}
void merge(int[] nums, int l, int r) {
if (l >= r) {//如果左邊大於右邊,此時就代表該部分只有一個數,不再需要進行遞歸操作
return;
}
int mid = l + (r - l) / 2;//中間位置計算
merge(nums, l, mid);//遞歸的分割數列的前半部分
merge(nums, mid + 1, r);//遞歸的分割數列的後半部分
merge_sort(nums, l, mid, r);//分割完之後就進行和並操作
}
void merge_sort(int[] nums, int l, int mid, int r) {
int[] temp = new int[r - l + 1];
System.arraycopy(nums, l, temp, 0, temp.length);
//初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
int i = l;
int j = mid + 1;
for (int k = l; k <= r; k++) {
if (i > mid) { // 如果左半部分元素已經全部處理完畢
nums[k] = temp[j - l];//直接將右半部分剩餘的數全部放入原數組中
j++;
} else if (j > r) { // 如果右半部分元素已經全部處理完畢
nums[k] = temp[i - l];//直接將左半部分剩餘的數全部放入原數組中
i++;
} else if (temp[i - l] > temp[j - l]) {//如果左半部分的值大於有半部分的值
nums[k] = temp[j - l];//將右半部分的值賦給原始數組
j++;
} else {
nums[k] = temp[i - l];//將左半部分的值賦給原始數組
i++;
}
}
}
代碼(迭代版):
void Sort(int[] nums) {
for (int size = 1; size <= nums.length; size += size) {//一輪循環的長度,就相當於把遞歸版的分解操作取消掉了
for (int j = 0; j + size < nums.length; j += size * 2) {//j代表左半部分,j+size代表有半部分
merge_sort(nums, j, j + size - 1, Math.min(j + size * 2 - 1, nums.length - 1));
// Math.min(j + size * 2 - 1, nums.length - 1)防止越界,如果size>(num.length/2)的話,r的值就會超過數組的總長度,這是就因該是num.length。
}
}
}
void merge_sort(int[] nums, int l, int mid, int r) {//該部分與上面一致
int[] temp = new int[r - l + 1];
System.arraycopy(nums, l, temp, 0, temp.length);
int i = l;
int j = mid + 1;
for (int k = l; k <= r; k++) {
if (i > mid) {
nums[k] = temp[j - l];
j++;
} else if (j > r) {
nums[k] = temp[i - l];
i++;
} else if (temp[i - l] > temp[j - l]) {
nums[k] = temp[j - l];
j++;
} else {
nums[k] = temp[i - l];
i++;
}
}
}
快速排序(基礎版)
快速排序就如它的名字一樣,排序速度非常快,是一種nlog(n)級別的排序算法,其核心思想與歸併排序一樣,也是分治思想,它從待排序的數組中選取一個數作爲基準數,然後將比它大的數放在它的右邊,比它小的數都放在它的左邊,不斷重複就可以完成整個數組的排序。
快速排序流程:
1.隨機選取數組中的一個數,將它作爲基準數;
2.設置兩個索引,一個用於遍歷數組中數的索引i,另一個是比基準數小的最後一個數位置的索引k;
3.從左邊開始遍歷,如果比基準數小,則將當前的數與第一個比基準數大的數位置交換,否則將i索引加一;
4.重複以上操作就將數組中的數分成了兩部分,左邊比基準數小,右邊比基準數大,而且基準數也在它應該在的位置;
5.接着進行遞歸操作,將上一次排序後的左半部分和右半部分分別進行上面的操作,直至全部數排序完成。
圖示:
代碼:
//遞歸調用
void quick_Sort(int[] nums, int l, int r) {
if (l >= r) {
return;
}
int p = partition(nums, l, r);//返回的索引左邊都比nums[p]小,右邊都比nums[p]大
quick_Sort(nums, l, p - 1);
quick_Sort(nums, p + 1, r);
}
//對nums[l...r]進行分類
//返回索引p,使得nums[l..p-1]都小於nums[p],右邊都大於num[p]
int partition(int[] nums, int l, int r) {
int k = l;//k表示比nums[p]小的最後一個元素,即nums[l....k]都是比temp小的數,nums[k+1.....r]都是大於等於temp的數
int temp = nums[l];
int random = (int) (Math.random() * (r - l + 1)) + l;//隨機算法進行優化
nums[l] = nums[random];
nums[random] = temp;
temp = nums[l];
for (int i = l + 1; i <= r; i++) {
if (nums[i] <= temp) {
int cache = nums[i];
nums[i] = nums[k + 1];
nums[k + 1] = cache;
k++;
}
}
nums[l] = nums[k];
nums[k] = temp;
return k;
}
快速排序(優化版)
以上版本的快排在遇到有大量重複元素的數組時,該算法就會退化到n^2級別,之所以會這樣,是因爲上一個版本如果遇到等於的數是直接將其放在了或者是左邊亦或是在右邊,會出現左右不平衡的狀態,這時候就需要進行優化,將等於的數進行左右分配平均一下,這樣就不會出現左右不平衡的情況,這也就是雙路快排的實現。
圖示:
代碼:
void quick_sort(int[] nums, int l, int r) {
if (l >= r) {
return;
}
int p = partition2(nums, l, r);
quick_sort(nums, l, p - 1);
quick_sort(nums, p + 1, r);
}
int partition2(int nums[], int l, int r) {
//隨機化處理
int random = RandomUtils.nextInt(l, r);
int temp = nums[random];
nums[random] = nums[l];
nums[l] = temp;
temp = nums[l];
int k = l + 1;//設置索引,從l到k的數都是小於等於temp的
int j = r;//設置索引,從j到r的數都是大於等於temp的
boolean is_left = true;//設置從那邊開始遍歷
while (k <= j) {//設置邊界條件
if (is_left) {
if (nums[k] >= temp) {
is_left = false;//如果nums[k]大於等於temp說明這個數需要放在右邊區域
} else {
k++;//否則k索引加一
}
} else {
if (nums[j] <= temp) {
is_left = true;//如果nums[k]小於等於temp說明該數需要放在左邊區域
int cache = nums[k];//交換兩者位置,即將之前遍歷到的大於等於temp的數與當前小於等於temp的數交換位置。
nums[k] = nums[j];
nums[j] = cache;
k++;
j--;
} else {
j--;
}
}
}
nums[l] = nums[j];//注意,該處是j的索引而不是k的索引,因爲遍歷到最後j與k會交換位置,即j=k-1,
// 此時j纔是比temp小的最後一個數,而k變成了比temp大的第一個數
nums[j] = temp;
return j;
}
快速排序(再優化)
基於上面的思想,我們還可以進一步優化,對於重複的元素,我們將其單獨分出來,即數組左邊一段比基準數小,中間一段和基準數一樣,右邊一段比基準數大,這樣數組就分成了三部分,這就是三路快排的基本思想。
圖示:
代碼:
void Sort(int[] nums) {
quick_sort(nums, 0, nums.length - 1);
}
void quick_sort(int[] nums, int l, int r) {
if (l >= r)
return;
int[] p = partitions(nums, l, r);
quick_sort(nums, l, p[0]);
quick_sort(nums, p[1], r);
}
int[] partitions(int[] nums, int l, int r) {
int random = RandomUtils.nextInt(l, r);
int temp = nums[l];
nums[l] = nums[random];
nums[random] = temp;
temp = nums[l];
int lt = l;//該索引表示[l...lt]都小於temp
int gt = r + 1;//該索引表示[gt...r]都大於temp
int i = lt + 1;//該索引表示[lt+1...i]都等於temp
while (i < gt) {//使用i使用進行遍歷
if (nums[i] < temp) {//如果比temp小,就與第一個等於temp的位置交換
int cache = nums[i];
nums[i] = nums[lt + 1];
nums[lt + 1] = cache;
i++;//同時i索引與lt索引都自增1
lt++;
} else if (nums[i] > temp) {//如果比temp大,就與第一個大於temp之前的位置交換
int cache = nums[i];
nums[i] = nums[gt - 1];
nums[gt - 1] = cache;
gt--;//同時gt索引要自減
} else {
i++;
}
}
nums[l] = nums[lt];
nums[lt] = temp;
return new int[]{lt, gt};
}
總結
以上就是這些主要排序的基本思路及實現,可能有的地方實現的方式與一些書上的實現方式有所不同,但大體的思路都是一樣的,其實算法的實現並不是十分的重要,重要的還是對算法思想的理解,思想理解了,實現的方法可以千差萬別。