五、冒泡排序
public void bubbleSort(int[] array){
for (int bound = 0; bound < array.length; bound++) {
for (int cur = array.length - 1; cur > bound ; cur--) {
if(array[cur - 1] > array[cur]){
swap(array,cur - 1,cur);
}
}
}
}
性能分析
1. 時間複雜度
最好:O(N) 數據有序
最壞:O(N^2) 數組逆序
平均:O(N^2)。
2.空間複雜度:O(1)
3.穩定性:不穩定
六、快速排序(重要)
原理
- 從待排序區間選擇一個數,作爲基準值(pivot);
- Partition: 遍歷整個待排序區間,將比基準值小的(可以包含相等的)放到基準值的左邊,將比基準值大的(可以包含相等的)放到基準值的右邊;
- 採用分治思想,對左右兩個小區間按照同樣的方式處理,直到小區間的長度 == 1,代表已經有序,或者小區間的長度 == 0,代表沒有數據。
從前往後,找第一個比基準值大的元素
從後往前,找第一個比基準值小的元素
交換這兩個位置的元素。
public void quickSort(int[] array){
//參數的含義表示針對數組中的那段區間進行排序
//由於有基準值,採用前閉後閉區間
quickSortHelper(array,0,array.length - 1);
}
public void quickSortHelper(int[] array,int left,int right){
if(left >= right){
//逼近條件很重要
//閉區間,有一個元素或者沒有元素
return;
}
//區間整理的方法:選取基準值,小於基準值的放在左側,大於基準值的放在右側
int index = partition(array,left,right);//整理完畢後基準值的下標
quickSortHelper(array,left,index - 1);
quickSortHelper(array,index + 1,right);
}
public int partition(int[] array,int left,int right){
//3w2s
int baseIndex = right;
int baseValue = array[baseIndex];//數組最右側元素,因爲是前閉後閉的
while (left < right){//聯想到swap
while (left < right && array[left] <= baseValue){
left++;
}
//走到這步,left指向的元素是從左往右走,第一個大於基準值的數
while (left < right && array[right] >= baseValue){
right--;
}
//走到這步,right指向的位置,就是第一個小於基準值的數
swap(array,left,right);
}
//循環結束,兩者重合
swap(array,left,baseIndex);
return left;
}
如何證明這裏的left對應的元素是比基準值大的
swap(array,left,baseIndex);
循環結束有兩種情況
- 由於right–導致的循環結束,此時就是right沒有找到比基準值小的元素,但是和left撞上了,並且left的元素是比基準值大的,此時跳出大循環,left指向的元素比基準值大
- 由於left++導致的循環結束,此時就是left沒有找到比基準值大的元素,但是和right撞上了,爲什麼此時的right還是比基準值大呢,因爲上一層循環left和right交換了。
如果要取左側的元素爲基準值,1.從後往前找比基準值小的2.從前往後找比基準值大的
(取右側的元素爲基準值,1.從前往後找比基準值大的2.從後往前找比基準值小的。
public int partition(int[] array,int left,int right){
//3w2s
int baseIndex = left;
int baseValue = array[baseIndex];//數組最右側元素,因爲是前閉後閉的
while (left < right){
while (left < right && array[right] >= baseValue){
right--;
}
//走到這步,right指向的位置,就是第一個小於基準值的數
while (left < right && array[left] <= baseValue){
left++;
}
//走到這步,left指向的元素是從左往右走,第一個大於基準值的數
swap(array,left,right);
}
//循環結束,兩者重合
swap(array,right,baseIndex);
return right;
}
性能分析
1. 時間複雜度:最壞O(N^2)(逆序,沒法達到遞歸二分的效果)
最好:O(N*log(N))
平均:O(N*log(N))
2.空間複雜度:由於進行了遞歸,需要調用棧保存層次的關係,需要佔據空間的
最好:O(N)
最壞:O(log(N))
平均:O(log(N))
穩定性:不穩定
快速排序的優化
- 如果當前逆序的話,用上面的快速排序效率低
- 如果基準值選的不好,也會影響性能
- 元素數量多,遞歸深度大,棧就存不下了
優化手段
1.優化取基準值:三元素取中(第一個元素,最後一個元素,中間位置的元素)
2.如果遞歸深度達到一定層次,不再遞歸,對當前的待排序區間使用其他排序算法(堆排序)
3.如果當前待排序區間比較小(left,right),使用插排
非遞歸實現快速排序
用棧的思想來實現遞歸的操作
代碼如下:
public void quickSortByLoop(int[] array){
//1.先創建一個棧,棧裏面存的是待處理區間的下標
Stack<Integer> stack = new Stack<>();
stack.push(array.length - 1);
stack.push(0);//此時根節點入棧
while (!stack.isEmpty()){
//3.取棧頂元素,棧頂元素就是我們要處理的區間
int left = stack.pop();//左側的區間
int right = stack.pop();
if(left >= right){
//一個元素或空,不符合要求,
continue;
}
int index = partition(array,left,right);
//接下來將左右兩個區間入棧
stack.push(index-1);
stack.push(left);
stack.push(right);
stack.push(index + 1);
}
}
用棧來模擬遞歸的過程
七、歸併排序
這和我之前寫過的一道題很像,叫做歸併兩個有序鏈表變成一個有序鏈表
public void mergeSort(int[] array){
//輔助方法,有三個參數,是區間(前閉後開)
mergeSortHelper(array,0,array.length);
}
public void mergeSortHelper(int[] array,int left,int right){
//類似於後序
if(left >= right || right - left == 1){
//劃分的數組爲空或着只有一個元素
return;
}
int mid = (left + right)/2;
//[left,mid)
//[mid,right)
mergeSortHelper(array,left,mid);//遞歸左區間數組
mergeSortHelper(array,mid,right);//遞歸右區間數組
merge(array,left,mid,right);//合併
}
public void merge(int[] array,int left,int mid ,int right){
//[left,mid)
//[mid,right)
int length = right - left;//創建數組的長度
int[] output = new int[length];
int outputIndex = 0;
int i = left;//想當於鏈表的cur1,cur2,記錄下標的位置
int j = mid;
while (i < mid && j < right){//類似cur1和cur2 都不爲空
if(array[i] <= array[j]){
output[outputIndex++] = array[i++];
}else {
//否則將後面的元素添入
output[outputIndex++] = array[j++];
}
}
while (i < mid ){
output[outputIndex++] = array[i++];
}
while (j < right){
output[outputIndex++] = array[j++];
}
for (int k = 0; k < length; k++) {
array[left + k] = output[k];
}
}
public static void main(String[] args) {
SortDemo sortDemo = new SortDemo();
int[] array ={9,5,2,7,6,5,1,8};
sortDemo.mergeSort(array);
System.out.println(Arrays.toString(array));
}
性能分析
1. 時間複雜度:O(n * log(n)) 數據不敏感
2. 空間複雜度:O(N) 數據不敏感
3. 穩定性:穩定(目前只有插入排序,冒泡排序和這個歸併排序是穩定的)
優化總結
在排序過程中重複利用兩個數組,減少元素的複製過程
歸併排序
1. 數據允許在外存中,外部排序,核心思路就是歸併排序
2. 歸併排序也是一種高效的給鏈表進行排序的算法
3. 也是各種標準庫中穩定排序算法的主要實現方式
非遞歸版本(瞭解即可)
藉助下標,對整個數組進行分組。
public void mergeSortByloop(int[] array){
//藉助下標及相關規律進行分組
//初始情況下,讓每個元素單獨作爲一組
//[0][1] [2][3] [4][5]
//[0,1] 和 [2,3] 合併 [4,5]和[6,7]合併
for (int gap = 1;gap < array.length;gap *= 2){
for (int i = 0; i < array.length; i += 2*gap) {
int beg = i;
int mid = i + gap;
int end = i + 2*gap;
if(mid > array.length){
mid = array.length;
}
if(end > array.length){
end = array.length;
}
merge(array,beg,mid,end);
}
}
}
排序方法 | 最好 | 平均 | 最壞 | 空間複雜度 | 穩定性 |
---|---|---|---|---|---|
冒泡排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 不穩定 |
插入排序 | O(N) | O(N^2) | O(N^2) | O(1) | 不穩定 |
選擇排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 穩定 |
希爾排序 | O(N) | O(N^1.3) | O(N^2) | O(1) | 不穩定 |
堆排序 | O(N*log(N)) | O(N*log(N)) | O(N*log(N)) | O(1) | 不穩定 |
快速排序 | O(N*log(N)) | O(N*log(N)) | O(N^2):逆序 | O(log(N))~O(N) | 不穩定 |
歸併排序 | O(N*log(N)) | O(N*log(N)) | O(N*log(N)) | O(N) | 穩定 |