排序算法總結(你想要的排序,這裏都有)

排序算法的總結

首先,對於所有的排序做一個總結,隨後是所有排序的實現方式

排序算法的穩定性

穩定性是指同樣大小的樣本再排序不會改變次序
對於基礎類型的排序
對於非基礎類型的排序來說,穩定性有重要的意義(比如結果需要年齡有序並且年齡相等身高有序)
有些排序算法可以實現穩定性,有的排序算法無論如何都無法實現穩定

速記 時間複雜度 額外空間複雜度 穩定性
選擇排序 最小最前 O(N²) O(1)
冒泡排序 最大最後 O(N²) O(1)
插入排序 逐步有序 O(N²) O(1)
歸併排序 二分遞歸 O(logN*N) O(N)
隨機快排 確認中點 O(logN*N) O(logN)
堆排序 建立堆結構 O(logN*N) O(1)
桶排序實現
計數排序 幾個元素幾個桶 O(N) O(M)
基數排序 O(N) O(1)
  1. 不基於比較的排序(桶排序),對樣本的數據有嚴格的要求,不易改寫
  2. 基於比較的排序,只要規定兩個樣本怎麼比較大小就能夠複用
  3. 基於比較的排序,算法的時間複雜度的極限是N*logN
  4. 時間複雜度爲O(logN*N),空間複雜度低於O(N),且具有穩定性的算法是不存在的
  5. 爲了絕對的速度,選擇快速排序,爲了省空間(常數時間操作長),選擇堆排序,爲了穩定性,選擇歸併排序

選擇排序

  1. arr[0-N-1]範圍上,找到最小值所在的位置,然後把最小值交換到0位置
  2. arr[1-N-1]範圍上,找到最小值所在的位置,然後把最小值交換到1位置
  3. arr[N-1~N-1]範圍上,找到最小值位置,然後把最小值交換到N-1位置。
    時間複雜度估計
    在這裏插入圖片描述
    總操作數爲等差數列
    算出快排的時間複雜度爲 n²/2 +n/2 其中忽略低階項和高階項係數得出時間負載度爲O(N²)

代碼實現

    public static void solution(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            int index = i;
            for (int j = i; j < arr.length; j++) {
                if (arr[j] < arr[index]) {
                    index = j;
                }
            }
            swap(arr, i, index);
        }
    }

冒泡排序

在arr[0~N-1]範圍上:

  1. arr[0]和arr[1],誰大誰來到1位置;arr[1]和arr[2],誰大誰來到2位置…arr[N-2]和arr[N-1],誰大誰來到N-1位置

  2. 在arr[0~N-2]範圍上,重複上面的過程,但最後一步是arr[N-3]和arr[N-2],誰大誰來到N-2位置

  3. 在arr[0~N-3]範圍上,重複上面的過程,但最後一步是arr[N-4]和arr[N-3],誰大誰來到N-3位置

  4. 最後在arr[0~1]範圍上,重複上面的過程,但最後一步是arr[0]和arr[1],誰大誰來到1位置

    public static void solution(int[] arr) {
        //在 0 -> N-1 看自己和下一個數那個小,大的往後面移動
        //0 ->N-2
        //0 ->N-3.....
        for (int i = arr.length - 1; i >= 0; i--) {
            for (int j = 0; j < i; j++) {
                if (arr[j] > arr[j + 1]) {
                    swap(arr, j, j + 1);
                }
            }
        }
    }
   	public static void swap(int[] arr, int i, int j) {
		int tmp = arr[i];
		arr[i] = arr[j];
		arr[j] = tmp;
	}
    

插入排序

  1. 想讓arr[0~0]上有序,這個範圍只有一個數,當然是有序的。
  2. 想讓arr[0~1]上有序,所以從arr[1]開始往前看,如果arr[1]<arr[0],就交換。否則什麼也不做。
  3. 想讓arr[0~i]上有序,所以從arr[i]開始往前看,arr[i]這個數不停向左移動,一直移動到左邊的數字不再比自己大,停止移動。
  4. 最後一步,想讓arr[0~N-1]上有序, arr[N-1]這個數不停向左移動,一直移動到左邊的數字不再比自己大,停止移動。

估算時發現這個算法流程的複雜程度,會因爲數據狀況的不同而不同。

如果數據是{1,2,3,4,5,6,7}這種本來就有序的數據集,那麼時間複雜度是O(N),但是最差情況的時間複雜度是等差數列,所以時間複雜度爲O(N²)

	public static void insertionSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		// 0~0 有序的
		// 0~i 想有序
		for (int i = 1; i < arr.length; i++) { // 0 ~ i 做到有序
			for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
				swap(arr, j, j + 1);
			}
		}
	}
   	public static void swap(int[] arr, int i, int j) {
		int tmp = arr[i];
		arr[i] = arr[j];
		arr[j] = tmp;
	}

歸併排序

  1. 整體是遞歸,左邊先排好序+右邊排好序+merge讓整體有序
  2. 讓整體有序的過程採用了排外序的方法
  3. 利用master公式來求時間複雜度
  4. 當然可以用非遞歸實現

時間複雜度爲O(N*logN)

	// 遞歸方法實現
	public static void mergeSort1(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		process(arr, 0, arr.length - 1);
	}

	// arr[L...R]範圍上,變成有序的
	// L...R    N    T(N) = 2*T(N/2) + O(N)  ->
	public static void process(int[] arr, int L, int R) {
		if (L == R) { // base case
			return;
		}
		int mid = L + ((R - L) >> 1);
		process(arr, L, mid);
		process(arr, mid + 1, R);
		merge(arr, L, mid, R);
	}

	public static void merge(int[] arr, int L, int M, int R) {
		int[] help = new int[R - L + 1];
		int i = 0;
		int p1 = L;
		int p2 = M + 1;
		while (p1 <= M && p2 <= R) {
			help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
		}
		// 要麼p1越界了,要麼p2越界了
		while (p1 <= M) {
			help[i++] = arr[p1++];
		}
		while (p2 <= R) {
			help[i++] = arr[p2++];
		}
		for (i = 0; i < help.length; i++) {
			arr[L + i] = help[i];
		}
	}
   	public static void swap(int[] arr, int i, int j) {
		int tmp = arr[i];
		arr[i] = arr[j];
		arr[j] = tmp;
	}

快速排序

partition過程

給定一個數組arr,和一個整數num。請吧小於等於num的數放在數組的左邊,大於num的數放在數組的右邊。

在arr[L…R]範圍上,進行快速排序的過程

  1. 在這個範圍上,隨機選一個數爲num
  2. 用num對該範圍進行partition。==num的範圍爲[a,b]
  3. 對arr[L…a-1]進行快速排序(遞歸)
  4. 對arr[b+1…R]進行快速排序(遞歸)
隨機快排時間複雜度
  1. 通過分析知道,劃分值約靠近中間,性能越好,越靠近兩邊,性能越差
  2. 隨機選一個數劃分的目的是把好和差變成概率事件
  3. 每一種情況都列出,會有每種情況的時間複雜度,概率都爲1/N
  4. 所有情況都考慮,那麼這種模型的長期期望就是時間複雜度

時間複雜度爲O(N*logN) 額外的空間複雜度爲O(logN)

	public static void quickSort3(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		process3(arr, 0, arr.length - 1);
	}

	public static void process3(int[] arr, int L, int R) {
		if (L >= R) {
			return;
		}
		swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
		int[] equalArea = netherlandsFlag(arr, L, R);
		process3(arr, L, equalArea[0] - 1);
		process3(arr, equalArea[1] + 1, R);
	}
	
	//  <arr[R]  ==arr[R]  > arr[R]
	public static int[] netherlandsFlag(int[] arr, int L, int R) {
		if (L > R) {
			return new int[] { -1, -1 };
		}
		if (L == R) {
			return new int[] { L, R };
		}
		int less = L - 1; // < 區 右邊界
		int more = R;     // > 區 左邊界
		int index = L;
		while (index < more) {
			if (arr[index] == arr[R]) {
				index++;
			} else if (arr[index] < arr[R]) {
				swap(arr, index++, ++less);
			} else { // >
				swap(arr, index, --more);
			}
		}
		swap(arr, more, R);
		return new int[] { less + 1, more };
	}
   	public static void swap(int[] arr, int i, int j) {
		int tmp = arr[i];
		arr[i] = arr[j];
		arr[j] = tmp;
	}

堆排序

堆結構

  1. 堆結構就是用數組實現的一顆完全二叉樹
  2. 完全二叉樹中如果每顆字樹的最大值都在頂部就是大根堆
  3. 完全二叉樹中如果每棵子樹的最小值都在頂部就是小根堆
  4. 堆結構的heapInsert與heapify操作
  5. 堆結構的增大和減少
  6. 優先級隊列結構就是堆結構
  public static void solution(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        //時間複雜度O(N*logN)
        for (int i = 0; i < arr.length; i++) {
            heapInsert(arr, i);
        }
        int heapSize = arr.length;
        swap(arr,0,--heapSize);
        //最大的數一直在數組第一個,然後把她與最後一個數交換,堆的大小減一
        while (heapSize > 0) { // O(N)
            heapify(arr, 0, heapSize); // O(logN)
            swap(arr, 0, --heapSize); // O(1)
        }

    }

    // arr[index]位置的數,能否往下移動
    public static void heapify(int[] arr, int index, int heapSize) {
        int left = index * 2 + 1; // 左孩子的下標
        while (left < heapSize) { // 下方還有孩子的時候
            // 兩個孩子中,誰的值大,把下標給largest
            // 1)只有左孩子,left -> largest
            // 2) 同時有左孩子和右孩子,右孩子的值<= 左孩子的值,left -> largest
            // 3) 同時有左孩子和右孩子並且右孩子的值> 左孩子的值, right -> largest
            int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
            // 父和較大的孩子之間,誰的值大,把下標給largest
            largest = arr[largest] > arr[index] ? largest : index;
            if (largest == index) {
                break;
            }
            swap(arr, largest, index);
            index = largest;
            left = index * 2 + 1;
        }
    }


    // arr[index]剛來的數,往上
    public static void heapInsert(int[] arr, int index) {
        while (arr[index] > arr[(index - 1) / 2]) {
            swap(arr, index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }


    public static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    public static void printArrat(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] +"\t");
        }
    }

桶排序

桶排序思想下的排序:計數排序和基數排序

  1. 桶排序思想下的排序都是不基於比較的排序
  2. 時間複雜度爲O(N),額外空間複雜度爲O(M)
  3. 應用範圍有限,需要更具樣本的數據狀況滿足桶的劃分

計數排序

根據年齡排序,建立1-200 的桶,將對應的元素放入對應的桶中。通過保證放入順序保持穩定性

    /**
     * 計數排序只適合值的範圍比較小的排序
     * 根據需要排序的數組的最大值,生成一個數組,將原數組中的值放入生成數組中對應索引的位置
     * 適合場景,排序年齡,根據年齡排序(使用對應多個桶)
     *
     * @param arr
     */
    public static void solution(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        int max = 0;
        for (int i = 0; i < arr.length; i++) {
            max = Math.max(max, arr[i]);
        }
        int[] help = new int[max + 1];
        for (int i = 0; i < arr.length; i++) {
            help[arr[i]] += 1;
        }
        int index = 0;
        for (int i = 0; i < help.length; i++) {
            for (int j = 0; j < help[i]; j++) {
                arr[index++] = i;
            }
        }
    } 

基數排序

對10進制的數字進行排序

 public static void solution(int[] arr, int L, int R, int dight) {
        int i = 0, j = 0;
        int[] help = new int[R - L + 1];
        //數字有多少位就使用多少次循環
        for (int d = 1; d <= dight; d++) {
            int[] cout = new int[10];
            //使用cout記錄每一位是對應數字的有多少
            for (i = L; i < R + 1; i++) {
                j = getDigit(arr[i], d);
                cout[j]++;
            }
            //把cout變爲小於等於該值的有多少
            for (int k = 1; k < cout.length; k++) {
                cout[k] += cout[k - 1];
            }
            //從後往前,如果該位是對應數字的話,使用cout中的值,把該數放在最大的位置上,因爲是從後往前,所以保證了穩定性
            for (int k = R; k >= L; k--) {
                int a = getDigit(arr[k], d);
                help[cout[a] - 1] = arr[k];
                cout[a]--;
            }
            //把help的值賦給原數組
            for (int k = L, c = 0; k <=  R; k++, c++) {
                arr[k] = help[c];
            }
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章