這篇文章用來複習一下數據結構中常提到的八大排序和三大查找算法
八大排序
八大排序都有哪些?
八大排序按照種類可以分爲以下幾種:
- 交換排序:冒泡,快排
- 插入排序:直接插入,希爾排序
- 選擇排序:直接選擇,堆排
- 歸併排序
- 基數排序
下面就按照順序對八大排序進行復習
交換類排序
冒泡
// 最簡化版的
public static void myBubbleSort(int[] arr) {
int realCount = 0;
// 外層循環控制冒出泡的個數,每循環一次就會冒出一個
for (int i = 0; i < arr.length; i++) {
// 內層循環進行相鄰冒泡操作 將最大的值冒泡到最右邊
for (int i1 = 0; i1 < arr.length - i - 1; i1++) {
realCount++;
if (arr[i1] > arr[i1 + 1]) {
swap(arr, i1, i1 + 1);
}
}
}
System.out.println(realCount);
for (int i : arr) {
System.out.print(i + " ");
}
}
public static void swap(int[] arr, int index1, int index2) {
int temp;
temp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = temp;
}
打印結果如下
使用Flag進行冒泡排序優化
// 思路:相鄰交換
// 優化:可採用flag進行循環優化,開始每次開始就設置爲false,如果有修改記錄就設置爲true,然後加入判斷條件
public static void myBubbleSort(int[] arr) {
boolean flag = true;
int realCount = 0;
// 外層循環控制冒出泡的個數,每循環一次就會冒出一個
for (int i = 0; i < arr.length; i++) {
int count = 0;
// 內層循環進行相鄰冒泡操作 將最大的值冒泡到最右邊
for (int i1 = 0; i1 < arr.length - i - 1 && flag; i1++) {
realCount++;
if (arr[i1] > arr[i1 + 1]) {
swap(arr, i1, i1 + 1);
flag = true;
count++;
}
if (count == 0) {
flag = false;
}
}
}
System.out.println(realCount);
for (int i : arr) {
System.out.print(i + " ");
}
}
public static void swap(int[] arr, int index1, int index2) {
int temp;
temp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = temp;
}
打印結果如下:可以看到比上一個少了五次交換
快排
快排就是快速排序,它的核心思想就是規定一個值爲基準值(需要注意的是不是基準指針),比基準值大的值交換到左邊,比基準值小的交換到右邊,如此往復遞歸就逐步確定順序了
public static void myQuickSort(int[] arr, int left, int right) {
int index = (left + right) / 2;
// 基準值
int pivot = arr[index];
int pl = left;
int pr = right;
// 滿足的條件是左指針小於右指針
while (pl < pr) {
// 找到左邊第一個比pivot大的值
while (arr[pl] < pivot) {
pl++;
}
// 從右邊找第一個比pivot小的值
while (arr[pr] > pivot) {
pr--;
}
// 判斷是否指針交叉
if (pl > pr) {
break;
}
// 那就swap
swap(arr,pl,pr);
// 如果是基準值位置改變
// 基準值跑到了右邊
if (arr[pr]==pivot){
pr--;
}
// 如果基準值跑到了左邊
if (arr[pl] == pivot){
pl++;
}
// 無誤就進行下一次循環,直到出現了指針交叉
}
// 遞歸調用 如果錯位了的話纔會到這裏,錯位時就相當於把pivot的值空了出來,也不用再次參加排序
if (pr>left){
myQuickSort(arr,left,pr);
}
if (pl<right){
myQuickSort(arr,pl,right);
}
}
public static void swap(int[] arr, int index1, int index2) {
int temp;
temp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = temp;
}
運行結果:
可以看到快速排序的效率和基準點的選取有關
常見的快排的幾種方式(最壞情況)
- 固定點
- 隨機取點
- 三數取中(left,mid,right)選擇次大的一個值,可以擴展爲多點取中,如五點取中
優化思路:可以將數組小於某個範圍後將它改爲插入排序,因爲此時數據都很幾乎很集中(但是這種優化思路對於本身有序的並沒有太大效果提升,適合隨機的,一般採用的是三數取中+插排)
if (right - left+1 <10){
// 插入排序
}else{
// 快排
}
插入類排序
直接插入
插入排序的核心思想就是在給定的數組內劃分出一個有序的區域和無序的區域,然後每一輪從無序的區域中取一個元素插入到有序區域
雖然這個排序很easy,但是編譯器完全不給我面子,把測試的舊的數組緩存給我存了好長時間,還以爲代碼寫錯了,改了好長時間都發現不了。
public static void myInsertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int temp = arr[i];
int j;
// 如果碰到一個比temp大的就往後移動,最後只需要記住temp需要插入的地方就行了
for (j = i - 1; j >= 0 && temp < arr[j]; j--) {
arr[j + 1] = arr[j];
}
arr[j+1] = temp;
}
}
下圖是我手寫的。當i指向的值是2的執行過程,也就是最外層循環執行的一次流程
希爾排序
希爾排序也叫縮小增量法,是加強版的插入排序,因爲希爾排序到增量爲1的時候就是插入排序
// 縮小增量 代碼上就是講插入排序的gap動態改變
public static void myShellSort(int[] arr) {
for (int gap = arr.length / 2; gap >= 1; gap /= 2) {
//下面就是變形的插入排序(只是插入排序的gap恆等於1)
for (int i = gap; i < arr.length; i++) {
int temp = arr[i];
int j;
// 容易出現錯誤的點就是此處的條件是j>=0,而不是j>=gap
for (j = i - gap; j >= 0 && temp<arr[j]; j-=gap) {
arr[j + gap] = arr[j];
}
arr[j + gap] = temp;
}
}
}
結果如下
選擇類排序
直接選擇排序
直接選擇排序就和它的名字一樣,它每一輪會選擇出一個最大的值/最小的值,如此往復循環就可以完成排序,代碼實現也是很簡單
public static void mySelectSort(int[] arr) {
// 外層控制的是進行的進度 每次找出一個最小值放在前面
for (int i = 0; i < arr.length; i++) {
int min = arr[i];
int index = i;
// 內存控制的是需要進行比較的輪次
for (int j = i + 1; j < arr.length; j++) {
// 需要記錄最小值的index就ok 默認i指的就是最小值
if (arr[j] < min) {
min = arr[j];
index = j;
}
}
arr[index] = arr[i];
arr[i] = min;
}
}
運行結果如下
堆排序
瞭解堆排序之前,先說說什麼是堆?
簡單來說
- 堆的首要條件就是要是一顆完全二叉樹,從數組層面來看就是數組的數據要是連續的
- 它的某個父節點對應的子節點總能小於或者是大於它的父節點。
因此,從堆的定義來看,堆可以分爲兩種類型,一種是大頂堆,另外一種就是小頂堆。所謂大頂堆就是最頂的節點它是最大的,所謂小頂堆就是最頂的節點它是最小的。
正是因爲堆必須是一個完全二叉樹,所有它對應到數組就會有如下兩個公式,這也是在構建堆時必不可少的已知條件。
公式如下:
對於任何一個下標爲n並且有子節點的節點都有如下公式:
- 左子節點下標 = 2n+1
- 右子節點下標 = 2n+2
由此也可以從子節點得到父節點的下標,這樣的關係就可以保證給出的數組就一定是一個堆,並且這個堆也是邏輯可控的。
在下面我就以我的測試用例逐步進行推導 —————— 用例:1,16,7,3,20,17,8
首先,根據這個數組,可以構建出一個邏輯上的不完全的堆,如下圖(畫的有點難看)
爲什麼說它是一個不完全的堆呢?因爲,它並完全不滿足堆的定義(不滿足第二條),因此就有了adjust方法來保證它是一個完整的堆。
adjust簡而言之就是調整,假如我們是大頂堆,對應下圖中下標爲0,1,2的一個樹該怎麼去調整,原理很簡單就是去比較,把最大的拿到根上就ok了。
因此,可以知道adjust方法就是一個三個數見比大小並交換位置。接着我們繼續往下,如果不斷擴大規模,如最開始那張不完整堆的圖,其實可以想到,都有一樣的道理,我們只需要去找到最後一個節點的父節點,然後一直倒序變量並調用adjust方法就可以完成對堆的維護。
如下圖
可以發現只需要對以下標2位根對應的子樹和以下標1位根保持大頂堆, 然後最後對以下標爲1的堆進行調整,這樣就可以保證大頂堆的構建了。根據調整就可以構建出如下的一個大頂堆結構了
package sort;
/*
* @Author Wrial
* @Date Created in 15:51 2020/3/21
* @Description 堆排序 堆排序分爲大頂堆和小頂堆,大頂堆一般對應的降序排列,小頂堆一般是用於升序排列
*/
public class HeapSort {
public static void main(String[] args) {
int[] nums = {16, 1, 7, 3, 20, 17, 8};
headSort(nums);
for (int num : nums) {
System.out.print(num + " ");
}
}
/**
* 堆排序
*/
public static void headSort(int[] list) {
//構造初始堆,從第一個非葉子節點開始調整,左右孩子節點中較大的交換到父節點中
for (int i = (list.length) / 2 - 1; i >= 0; i--) {
headAdjust(list, list.length, i);
}
//排序,將最大的節點放在堆尾,然後從根節點重新調整
for (int i = list.length - 1; i >= 1; i--) {
int temp = list[0];
list[0] = list[i];
list[i] = temp;
headAdjust(list, i, 0);
}
}
/**
* @param list arr
* @param len 當前這個堆的大小,因爲在後面進行堆調整的時候會變
* @param i 需要調整的下標
*/
private static void headAdjust(int[] list, int len, int i) {
int k = i, temp = list[i], index = 2 * k + 1;
// 當子節點下標<len 也就是說它要是還有左子節點的話
while (index < len) {
// 如果有右子節點
if (index + 1 <= len - 1) {
// 如果左子節點小於右子節點
if (list[index] < list[index + 1]) {
// 就講指針指向右子節點
index = index + 1;
}
}
// 如果當前的子節點中最大的值大於父節點的值,就進行替換
if (list[index] > temp) {
list[k] = list[index];
// 並且將index更新到被修改的子節點上進行調整,直到break
k = index;
index = 2 * k + 1;
} else {
break;
}
}
// 要清楚的是引起堆變化的節點就說明它是相對小的節點,空出的位置必然是它,因此在上面只需要往上填,下面的值一定是這個temp
list[k] = temp;
}
}
結果演示:
歸併排序
歸併排序的算法思想其實就是分治算法,什麼是分治算法呢?簡單的來說就是先分後治,大而化小,最後合併。具體流程根據這組測試用例來完成------100,-5,8,1,9
分解過程,需要注意的是,這個分解過程並不是真實存在的,只是通過遞歸進行的邏輯分解
合併過程,合併過程是根據遞歸的邏輯分解,創建臨時數組完成合並後再拷貝到原數組
// 先分再合
/**
* @param arr 待排序數組
* @param left 左指針
* @param right 右指針
*/
public static void myMergeSort(int[] arr, int left, int right) {
// 首先要確定不能再分的條件right==left情況是本來有下標0和1,現在分了就剩下一個元素了,也不可分
// 剩下了一個元素,length/2=0,左指針下標爲0,右指針需要是length-1=-1(不滿足)
if (right > left) {
int mid = (left + right) / 2;
myMergeSort(arr, left, mid);
myMergeSort(arr, mid + 1, right);
myMerge(arr, mid, left, right);
}
}
/**
* @param arr 原數組
* @param mid 中間分割點
* @param left 左指針
* @param right 右指針
*/
public static void myMerge(int[] arr, int mid, int left, int right) {
// 建立臨時數組,將值拷貝過來
int[] temp1 = new int[mid - left + 1];
int[] temp2 = new int[right - mid];
for (int i = left; i <= mid; i++) {
temp1[i-left] = arr[i];
}
for (int i = mid + 1; i <= right; i++) {
temp2[i - mid - 1] = arr[i];
}
// 對有序結構進行插入排序,將每個數組最小的值放在最前面
int tempLeft = 0;
int tempRight = 0;
int arrayIndex = left;
while (tempLeft < temp1.length && tempRight < temp2.length) {
if (temp1[tempLeft]<= temp2[tempRight]){
arr[arrayIndex] = temp1[tempLeft];
arrayIndex++;
tempLeft++;
}else {
arr[arrayIndex] = temp2[tempRight];
arrayIndex++;
tempRight++;
}
}
while (tempLeft<temp1.length){
arr[arrayIndex] = temp1[tempLeft];
arrayIndex++;
tempLeft++;
}
while (tempRight<temp2.length){
arr[arrayIndex] = temp1[tempRight];
arrayIndex++;
tempRight++;
}
public static void main(String[] args) {
int[] arr1 = {100, -5, 8, 1, 9};
System.out.println(Arrays.toString(arr1));
myMergeSort(arr1, 0, arr1.length - 1);
System.out.println(Arrays.toString(arr1));
}
}
結果如下:
基數排序
基數排序的核心思想感覺有點像堆排序的從小到大建堆(自我感覺),先保持局部有序,再保證全體有序。思想就是從最小位數開始比較,然後放到指定的桶裏,接着比較高一位如此往復
以以下數據爲例——5, 2, 33, 0, 9, 562, 4,
第一輪:比較個位併入桶 取出的結果爲0 2 562 33 4 5 9
第二輪:比較百位,取出的結果0 2 4 5 9 33 562
第三輪::比較百位併入桶,由於最高只有百位所以結束,結果爲0 2 4 5 9 33 562
這樣就完成了桶排序也就是基數排序
因此桶排序需要的空間是一個二維數組,而不是一維數組
代碼如下
public static void main(String[] args) {
int[] arr = {5, 2, 33, 0, 9, 562, 4, 6, 213, 1, 111};
sort(arr);
System.out.println(Arrays.toString(arr));
}
//根據個位十位和百位進行分桶排序(先個位,十位....)
public static void sort(int[] arr) {
//1.獲取數組中最大數的位數
int max = arr[0];
int maxLength = 0;
int index;
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
String maxTemp = max + "";
//間接求出最大數的長度
maxLength = maxTemp.length();
System.out.println(maxLength);
//2.建立10個桶(十列),桶的大小就是數組的長度
int[][] barrels = new int[10][arr.length];
//3.需要一個桶計數器,計數各個桶裏有幾個數字 並進行初始化
int[] barrelCounter = new int[10];
for (int i = barrelCounter.length - 1; i >= 0; i--) {
barrelCounter[i] = 0;
}
System.out.println("桶計數器初始化"+Arrays.toString(barrelCounter));
//4.將數放入桶,從低到高
// 333/1%10 333/10%10 333/100%10
for (int j = 0, n = 1; j < maxLength; j++, n *= n) {
System.out.println("第"+(j+1)+"輪開始");
//放數字到相應的桶
for (int i = 0; i < arr.length; i++) {
int digital = arr[i] / n % 10;
barrels[digital][barrelCounter[digital]] = digital;
barrelCounter[digital]++;
}
index = 0;
//5.每放完一輪將數字再拿出來
for (int i = 0; i < 10; i++) {
if (barrelCounter[i] != 0) { //說明有數字
for (int k = 0; k < barrelCounter[i]; k++) {
arr[index] = barrels[i][k];
index++;
}
}
}
//6.計數清零
for (int i = 0; i < barrelCounter.length; i++) {
barrelCounter[i] = 0;
}
System.out.println("第"+(j+1)+"輪結束");
}
}
結果如下:
這就是八大排序的所有內容!