1. 算法實現
排序中比較復雜的有歸並排序,快速排序,堆排序三大算法了,三個算法的時間復雜度都是O(N * logN)
,三個算法的思想我就簡單的展開詳述以下。
1.1 歸並排序
歸並排序的核心思想是鏈表中的經典題目:合並兩個有序鏈表。
Leetcode P21: Merge Two Sorted Lists
采用分治的思想,將整個數組分為兩個部分,先對左邊的數組進行歸並排序,再對右邊的數組進行歸並排序,最后兩者進行merge
。
下面的函數就是歸並排序的歸並部分,將兩個已經有序的數組歸並。
public class MergeSort implements Sort{
@Override
public void sort(int[] arr) {
if (arr == null || arr.length <= 1) return;
mergeSort(arr, 0, arr.length - 1);
}
public void mergeSort(int[] arr, int l, int r) {
if (l >= r) return;
int mid = l + ((r - l) >> 1);
//注意遞歸是i
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
merge(arr, l, mid, r);
}
public void merge(int[] arr, int l, int mid, int r) {
int[] newArr = new int[r - l + 1];
int left = l;
int right = mid + 1;
int index = 0;
while (left <= mid && right <= r) {
if (arr[left] > arr[right]) {
newArr[index++] = arr[right++];
} else {
newArr[index++] = arr[left++];
}
}
while (left <= mid) {
newArr[index++] = arr[left++];
}
while (right <= r) {
newArr[index++] = arr[right++];
}
for (int i = 0; i < newArr.length; i++) {
arr[i + l] = newArr[i];
}
}
}
1.2 堆排序
堆排序首先就要利用堆這個數據結構。首先先將整個數組結構調整成堆。
heapInset
函數插入某個數字,並且將插入的部分進行調整。對於一個數組,從位置0開始插入,並且邊插入邊進行調整。
public class HeapSort implements Sort{
@Override
public void sort(int[] arr) {
if (arr == null || arr.length <= 1) return;
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
int heapSize = arr.length;
swap(arr, 0, --heapSize);
while (heapSize > 0) {
heapify(arr, 0, heapSize);
swap(arr, 0, --heapSize);
}
}
public void heapInsert(int[] arr, int index) {
//index > 0 的條件可以省略,因為如果index是1的話,兩個值必定相等,無法進入循環
while (arr[(index - 1) / 2] < arr[index]) {
int fa = (index - 1) / 2;
swap(arr, index, fa);
index = fa;
}
}
public void heapify(int[]arr, int index, int heapSize) {
int left = index * 2 + 1;
while (left < heapSize) {
int right = left + 1;
int maxLeftRightIndex = right < heapSize && arr[left] < arr[right] ? right : left;
int maxIndex = arr[maxLeftRightIndex] > arr[index] ? maxLeftRightIndex : index;
if (maxIndex == index) {
break;
}
swap(arr, index, maxIndex);
index = maxIndex;
left = maxIndex * 2 + 1;
}
}
public void swap(int []arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
這樣處理完之后,數組就成為了一個最大堆。要將數組調整為有序的,要經歷以下幾步:
- 數組的第一個數是最大堆的頂,是最大的數,每次將第一個數和最大堆的最后一個數交換。這樣后面部分就排好序了
- 交換完之后,最大堆的結構就會被破壞了,所以需要調整最大堆。
//交換到最后一個位置
swap(arr, 0, --heapSize);
while (heapSize > 0) {
//調整位置成最大堆
heapify(arr, 0, heapSize);
//之后再次進行交換
swap(arr, 0, --heapSize);
}
調整位置
public void heapify(int[]arr, int index, int heapSize) {
int left = index * 2 + 1;
while (left < heapSize) {
int right = left + 1;
int maxLeftRightIndex = right < heapSize && arr[left] < arr[right] ? right : left;
int maxIndex = arr[maxLeftRightIndex] > arr[index] ? maxLeftRightIndex : index;
if (maxIndex == index) {
break;
}
swap(arr, index, maxIndex);
index = maxIndex;
left = maxIndex * 2 + 1;
}
}
1.3 快速排序
快速排序的核心思想思想是選一個數,不管是隨機的還是固定的,每一步將大於這個數的數字放在數組的右邊,小於的所有數字放在左邊,聽起來是不是很熟悉,這就是經典題目荷蘭國旗的思想啦。
public class QuickSort implements Sort{
@Override
public void sort(int[] arr) {
if (arr == null || arr.length <= 1) return;
int length = arr.length;
quickSort(arr, 0, length - 1);
}
public void quickSort(int[] arr, int l, int r) {
if (l >= r || l < 0) return;
int[] mids = partition(arr, l, r);
quickSort(arr, l, mids[0]);
quickSort(arr, mids[1], r);
}
public int[] partition(int arr[], int l, int r) {
int[] mids = new int[]{l ,r};
int num = arr[r];
int left = l - 1;
int right = r;
int cur = l;
while (cur < right) {
if (arr[cur] < num) {
swap(arr, ++left, cur++);
} else if (arr[cur] > num) {
swap(arr, --right, cur);
} else {
cur++;
}
}
swap(arr, right++, r);
mids[0] = left;
mids[1] = right;
return mids;
}
public void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = new int[]{4,1,2,3,4,3,1,4,1,1,23,4,5,4,2,2,3,5,3,1,1,2,3,4,5,6,7};
QuickSort q = new QuickSort();
q.sort(arr);
}
}
2. 時間復雜度分析
2.1 Master theorem
Master
公式是為了評估遞歸函數的復雜度而誕生的。
T(N) = a* T(N / 2) + O(N^d)
log(b,a) > d
時,時間復雜度是O(N ^ log(b,a))
log(b,a) = d
時,時間復雜度是O(N ^ d + logN)
log(b,a) < d
時,時間復雜度是O(N^d)
2.2 算法復雜度分析
使用master
公式計算三個排序的時間復雜度
2.2.1 歸並排序
Merge-Sort
的核心代碼如下:
public void mergeSort(int[] arr, int l, int r) {
if (l >= r) return;
int mid = l + ((r - l) >> 1);
(1)part 1
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
(2)part 2
merge(arr, l, mid, r);
}
時間復雜度估算:
part 1
部分將問題分為兩個部分,所以master
中a=2,b=2
。part 2
部分是有序鏈表的合並,時間復雜度是:O(N)
,所以d=1
。
所以套用master
公式中的第二條,時間復雜度是O(N*logN)
。
2.2.2 堆排序
Merge-Sort
的核心代碼如下:
public void sort(int[] arr) {
if (arr == null || arr.length <= 1) return;
(1)part1
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
int heapSize = arr.length;
(2)part2
swap(arr, 0, --heapSize);
while (heapSize > 0) {
heapify(arr, 0, heapSize);
swap(arr, 0, --heapSize);
}
}
時間復雜度估算:
- 最大堆的插入后調整的時間復雜度是
O(logN)
,part 1
部分是插入N
個元素,時間復雜度是log1+log2+log3+log4+...+logN = N
,所以part 1
的時間復雜度是O(N)
。 part 2
部分中heapify
的時間復雜度是logN
,heapify
就是堆的調整,所以part 2
部分的時間復雜度是O(N * logN)
。- 加起來就是
O(N)+O(N*logN)
,取最大項即為O(N*logN)
。
2.2.3 快速排序
Quick-Sort
的核心代碼如下:
public void quickSort(int[] arr, int l, int r) {
if (l >= r || l < 0) return;
(1)part 1
int[] mids = partition(arr, l, r);
(2)part 2
quickSort(arr, l, mids[0]);
quickSort(arr, mids[1], r);
}
時間復雜度估算:
part 1
中的partition
思想是荷蘭國旗問題,時間復雜度是O(N)
。part 2
和歸並排序一樣,也是將問題分為了兩個部分,所以a=2,b=2
。
所以快速排序的時間復雜度是O(N*logN)
。
2.2.4 空間復雜度
除了歸並排序中,需要提前分配O(N)
的空間,作為Merge
過程中的緩存。堆排序和快速排序全部都是in-place
修改的,所以不需要分配額外空間。
其實在在歸並排序和堆排序過程中,因為函數遞歸,函數占用的棧地址也算空間復雜度,但是為什么沒有算進去呢?個人認為是因為在一些高手的眼中,遞歸函數是可以改成循環,所以這就不用考慮這些額外空間了。