算法概述
算法分類
十種常見排序算法可以分爲兩大類:
- 比較類排序:通過比較來決定元素間的相對次序,由於其時間複雜度不能突破O(nlogn),因此也稱爲非線性時間比較類排序。
- 非比較類排序:不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間運行,因此也稱爲線性時間非比較類排序。
算法複雜度
相關概念
- 穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面。
- 不穩定:如果a原本在b的前面,而a=b,排序之後 a 可能會出現在 b 的後面。
- 時間複雜度:對排序數據的總的操作次數。反映當n變化時,操作次數呈現什麼規律。
- 空間複雜度:是指算法在計算機內執行時所需存儲空間的度量,它也是數據規模n的函數。
冒泡排序(Bubble Sort)
動圖演示
Java實現
/**
* 冒泡排序 平均時間複雜度爲o(n2),最好複雜度爲o(n),空間複雜度爲o(1)
*
* @author monkJay
*/
public class BubbleSort {
public static void solution(int[] array) {
int len = array.length;
// 從第一個開始直到倒數第二個
for (int i = 0; i < len - 1; ++i) {
// 每比較一輪,都會有一個已排序好的數字,i就是已經排序好了的個數
for (int j = 1; j < len - i; ++j) {
// 如果後面的數字小於前面的數字,則交換
if (array[j] < array[j - 1]) {
int tmp = array[j];
array[j] = array[j - 1];
array[j - 1] = tmp;
}
}
}
}
public static void main(String[] args) {
int[] array = new int[] { 2, 1, 4, 3, 6, 5, 9, 8 };
solution(array);
for (int i = 0; i < array.length; ++i) {
System.out.print(array[i] + " ");
}
}
}
選擇排序(Selection Sort)
動圖演示
Java實現
/**
* 選擇排序(最穩定的排序算法,不算什麼序列時間複雜度都是o(n2)) 每次從未排序的序列中選擇一個最小(大)的數放在已排序的序列的後面
* 平均時間複雜度爲o(n2),最好複雜度爲o(n2),空間複雜度爲o(1)
*
* @author monkJay
*/
public class SelectionSort {
public static void solution(int[] array) {
int len = array.length;
for (int i = 0; i < len - 1; ++i) {
int minIndex = i;
for (int j = i + 1; j < len; ++j) {
if (array[minIndex] > array[j]) {
minIndex = j;
}
}
int tmp = array[minIndex];
array[minIndex] = array[i];
array[i] = tmp;
}
}
}
插入排序(Insertion Sort)
動圖演示
Java實現
/**
* 插入排序 | 選擇未排序序列的第一個數,插入到已排序序列中合適得位置(從後往前找,直到找到一個比當前數小或者等於的數,插在它後面)
* 平均時間複雜度爲o(n2),最好複雜度爲o(n),空間複雜度爲o(1)
*
* @author monkJay
*/
public class InsertionSort {
public static void solution(int[] array) {
int len = array.length;
for (int i = 1; i < len; ++i) {
int preIndex = i - 1;
int current = array[i];
while (preIndex >= 0 && array[preIndex] > current) {
// 如果有序序列中的數大於要插入的數,則後移一個位置(將自己的位置空出來)
array[preIndex + 1] = array[preIndex];
// 繼續往前尋找
--preIndex;
}
// 找到了適合的位置(終於找到一個小於等於要插入的數了),將數字插在這個位置後面
array[preIndex + 1] = current;
}
}
}
希爾排序(Shell Sort)
動圖演示
Java實現
/**
* 希爾排序 將一個序列在邏輯上根據指定的步長分組,對每個分組進行插入排序(插入排序時的移動是根據當前步長移動查找的)
*
* @author monkJay
*/
public class ShellSort {
public static void solution(int[] array) {
int len = array.length;
// 步長從當前長度的一半開始,每一輪將步長減爲一半,直到最後小於1結束
for (int gap = len / 2; gap > 0; gap /= 2) {
// 取值從步長開始,這裏其實就是將直接插入排序中的1全部換成了步長gap
// 其它和直接插入排序一樣
for (int i = gap; i < len; ++i) {
int preIndex = i - gap;
int current = array[i];
while (preIndex >= 0 && array[preIndex] > current) {
array[preIndex + gap] = array[preIndex];
preIndex = preIndex - gap;
}
array[preIndex + gap] = current;
}
}
}
}
歸併排序(Merge Sort)
動圖演示
Java實現
/**
* 二路歸併排序 用到了分治的思想,不斷往下細分,分到只有一個數字(認爲是有序的)的時候開始合併,最後合併爲一個有序的數組
*
* @author monkJay
*/
public class MergeSort {
public static void solution(int[] array) {
int len = array.length;
if (len == 0) {
return;
}
int left = 0;
int right = len - 1;
int[] tmp = new int[len];
mergeSort(array, tmp, left, right);
}
public static void mergeSort(int[] array, int[] tmp, int left, int right) {
// 如果還可以分組,其實最後是分到只有一個數(認爲一個數是有序的)
if (left < right) {
int center = left + (right - left) / 2;
// 左邊的數組繼續往下分
mergeSort(array, tmp, left, center);
// 右邊的數組繼續往下分
mergeSort(array, tmp, center + 1, right);
// 將左右兩個有序的數組合並
merge(array, tmp, left, center, right);
}
}
public static void merge(int[] array, int[] tmp, int left, int center,
int right) {
int i = left, j = center + 1;
// 將要合併的兩個有序數組,從小到大有序的放在輔助數組中
for (int k = left; k <= right; ++k) {
// 如果左邊的數組遍歷完了,就直接放右邊的數組
if (i > center) {
tmp[k] = array[j++];
}
// 如果右邊的數組遍歷完了,就直接放左邊的數組
else if (j > right) {
tmp[k] = array[i++];
}
// 將小的放在輔助數組中
else if (array[i] <= array[j]) {
tmp[k] = array[i++];
} else {
tmp[k] = array[j++];
}
}
// 將輔助數組中的值複製到原數組中
for (int k = left; k <= right; ++k) {
array[k] = tmp[k];
}
}
}
快速排序(Quick Sort)
動圖演示
Java實現
/**
* 快速排序
* 通過一趟排序將待排記錄分隔成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分的關鍵字小,
* 則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序。
* @author monkJay
*/
public class QuickSort {
public static void solution(int[] array) {
int len = array.length;
if (len == 0) {
return;
}
int left = 0, right = len - 1;
quickSort(array, left, right);
}
public static void quickSort(int[] array, int left, int right) {
if (left < right) {
int partitionIndex = partition(array, left, right);
quickSort(array, left, partitionIndex - 1);
quickSort(array, partitionIndex + 1, right);
}
}
// 第一種分區方法
public static int partition(int[] array, int left, int right) {
// 選取左邊第一個數作爲中軸數
int pivot = left;
// 從中軸數後面一個位置開始比較
int index = pivot + 1;
// 保證指針左邊的數都是小於中軸數的
for (int i = left + 1; i <= right; i++) {
// 如果小於中軸數,將當前數的位置和中軸指針交換
if (array[i] < array[pivot]) {
swap(array, i, index);
// 指針繼續後移
++index;
}
}
// 將中軸數的位置和指針左邊一個位置交換,也就是把中軸數換到中間
swap(array, pivot, index - 1);
// 最後返回這個中軸數的位置
return index - 1;
}
// 第二種分區方法
// public static int partition(int[] array, int left, int right) {
// int i = left, j = right + 1;
// int pivot = array[left];
// while (i < j) {
//
// while (array[--j] > pivot && i < j)
// ;
// while (array[++i] < pivot && i < j)
// ;
// if (i < j) {
// swap(array, i, j);
// }
// }
// swap(array, left, j);
// return j;
// }
public static void swap(int[] array, int a, int b) {
int tmp = array[a];
array[a] = array[b];
array[b] = tmp;
}
}
堆排序(Heap Sort)
動圖演示
Java實現
/**
* 堆排序 先構建一個最大(小)堆,每次都堆頂取一個數,並把最後一個數交換到堆頂,然後重新調整堆,重複,直到堆中最後一個數
* 最大堆得到從小到大的排序,最小堆得到從大到小的排序
*
* @author HP
*
*/
public class HeapSort {
public static void solution(int[] array) {
int len = array.length;
if (len == 0) {
return;
}
buildHeap(array);
heapSort(array);
}
/**
* 下沉調整 用於堆的刪除,還可用於構建二叉堆
*
* @param array 存放堆的數組
* @param parentIndex 父結點位置
* @param len 數組長度
*/
public static void downAdjust(int[] array, int parentIndex, int len) {
// 先計算得到左孩子
int childIndex = parentIndex * 2 + 1;
// 保存父結點的值
int tmp = array[parentIndex];
while (childIndex < len) {
// 如果右孩子存在,且右孩子的值比左孩子要小,則將孩子節點定位到右孩子
if (childIndex + 1 < len
&& array[childIndex + 1] < array[childIndex]) {
childIndex++;
}
// 如果父結點的值比兩個孩子都小,不用調整了,直接結束
if (tmp < array[childIndex]) {
break;
}
// 將孩子節點的值賦給父結點,繼續下沉
array[parentIndex] = array[childIndex];
// 將孩子節點作爲新的父結點
parentIndex = childIndex;
// 重新計算左孩子的位置
childIndex = parentIndex * 2 + 1;
}
// 節點調整結束,將最初需要調整的那個節點賦值到最終位置
array[parentIndex] = tmp;
}
/**
* 其實就是讓每個非葉子節點依次下沉
* 構造堆 時間複雜度是線性的
* @param array 要構造堆的數組
*/
public static void buildHeap(int[] array) {
int len = array.length;
// 找到最後一個節點的父結點
int startIndex = (len - 2) / 2;
for (int i = startIndex; i >= 0; --i) {
downAdjust(array, i, len);
}
}
/**
* 堆排序 每次都堆頂取一個數,並把最後一個數交換到堆頂,然後重新調整堆,重複,直到堆中最後一個數
* @param array
*/
public static void heapSort(int[] array) {
for (int i = array.length - 1; i > 0; --i) {
int tmp = array[0];
array[0] = array[i];
array[i] = tmp;
// 從堆頂開始調整
downAdjust(array, 0, i);
}
}
}
計數排序(Counting Sort)
動圖演示
Java實現
/**
* 計數排序
* 計數排序不是基於比較的排序算法,其核心在於將輸入的數據值轉化爲鍵存儲在額外開闢的數組空間中。
* 作爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是有確定範圍的整數。
* @author HP
*/
public class CountingSort {
public static void solution(int[] array) {
int len = array.length;
if (len == 0) {
return;
}
int maxValue = 0;
// 得到最大值
for (int i = 0; i < len; ++i) {
if (array[i] > maxValue) {
maxValue = array[i];
}
}
int[] bucket = new int[maxValue + 1];
// 將桶用0填充
Arrays.fill(bucket, 0);
// 記錄每個元素出現的個數
for (int i = 0; i < len; ++i) {
bucket[array[i]]++;
}
int index = 0;
for (int i = 0; i < bucket.length; ++i) {
// 將桶中的元素往原數組放
while (bucket[i] > 0) {
array[index++] = i;
bucket[i]--;
}
}
}
}
topK的四種解法
構造大頂堆
算法分析:用數組前k個數構造一個大頂堆,然後依次比較後面的數,如果小於堆頂,則換掉,重新調整堆,最後的堆頂元素就是第k大的元素。時間複雜度爲O(klogk)。
Java實現
public class TopK {
public static int topK_Solution(int[] input, int k) {
int len = input.length;
if (len == 0 || k <= 0 || k > len) {
return 0;
}
// 先把前k個數構造成一個堆
int[] array = Arrays.copyOfRange(input, 0, k);
buildHeap(array);
// 遍歷後面的數,如果小於堆頂元素,就把堆頂換掉,並重新下沉調整堆
for (int i = k; i < len; ++i) {
if (input[i] < array[0]) {
array[0] = input[i];
downAdjust(array, 0, array.length);
}
}
return array[0];
}
// 構造堆
public static void buildHeap(int[] array) {
int startIndex = (array.length - 2) / 2;
for (int i = startIndex; i >= 0; --i) {
downAdjust(array, i, array.length);
}
}
// 調整最大堆
public static void downAdjust(int[] array, int parentIndex, int len) {
int tmp = array[parentIndex];
int childIndex = parentIndex * 2 + 1;
while (childIndex < len) {
if (childIndex + 1 < len
&& array[childIndex + 1] > array[childIndex]) {
childIndex++;
}
if (tmp > array[childIndex]) {
break;
}
array[parentIndex] = array[childIndex];
parentIndex = childIndex;
childIndex = parentIndex * 2 + 1;
}
array[parentIndex] = tmp;
}
public static void main(String[] args) {
int[] array = new int[] { 2, 1, 4, 3, 6, 5, 7, 9, 8 };
int res = GetLeastNumbers_Solution(array, 5);
System.out.print("第K大的元素是:" + res);
}
}
構造小頂堆
算法分析:先把數組構造成小頂堆,然後對堆進行k - 1次刪除堆頂的操作,最後的堆頂就是要求的第k大元素 。時間複雜度爲O(klogN)。
public class Solution {
public int topK_Solution(int [] input, int k) {
int len = input.length;
if (len == 0 || k <= 0 || k > len){
return 0;
}
// 先構造一個堆
bulidHeap(input);
// 將數組進行k - 1次刪除堆頂操作
for (int i = len - 1; i > len - k; --i){
int tmp = input[0];
input[0] = input[i];
input[i] = tmp;
downAdjust(input, 0, i);
}
// 返回最後的堆頂
return input[0];
}
public void bulidHeap(int[] array){
int startIndex = (array.length - 2) / 2;
for (int i = startIndex; i >= 0; --i){
downAdjust(array, i, array.length);
}
}
public void downAdjust(int[] array, int parentIndex, int len){
int tmp = array[parentIndex];
int childIndex = parentIndex * 2 + 1;
while (childIndex < len){
if (childIndex + 1 < len && array[childIndex + 1] < array[childIndex]){
childIndex ++;
}
if (tmp < array[childIndex]){
break;
}
array[parentIndex] = array[childIndex];
parentIndex = childIndex;
childIndex = parentIndex * 2 + 1;
}
array[parentIndex] = tmp;
}
}
使用Java的優先隊列
算法分析:其實跟前面用堆是一樣的,只不過用了Java中已經封裝好的堆,代碼更簡潔(但這裏貌似耗時要長一點,其實這裏刪除堆頂不用調整,但它這裏每次都調整了,所以浪費了時間)
public class Solution {
public int topK_Solution(int [] input, int k) {
int len = input.length;
if (len == 0 || k <= 0 || k > len){
return 0;
}
// 使用優先隊列構造一個最大堆,這裏用了lambda表達式
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(k, (o1, o2) -> {
return o2.compareTo(o1);
});
for (int i = 0; i < len; ++i) {
// 往堆中添加數字,直到堆的大小爲k
if (maxHeap.size() != k) {
maxHeap.offer(input[i]);
}
// 如果當前元素比堆頂的元素要小,那麼將堆頂出隊列,將當前元素加入隊列(內部會自動調整堆)
else if (input[i] < maxHeap.peek()) {
maxHeap.poll();
maxHeap.offer(input[i]);
}
}
// 返回堆頂元素
return maxHeap.peek();
}
}
利用快排的思想
算法分析:基於數組的第k個數來調整,使得最後小於k的數都在k的左邊,大於k的數都在k的右邊,那麼k就是第k大元素
public class Solution {
public int topK_Solution(int [] input, int k) {
int len = input.length;
if (len == 0 || k <= 0 || k > len){
return 0;
}
int start = 0;
int end = len - 1;
int index = partition(input, start, end);
while (index != k - 1){
if (index > k - 1){
end = index - 1;
index = partition(input, start, end);
} else {
start = index + 1;
index = partition(input, start, end);
}
}
return input[k - 1];
}
public int partition(int[] input, int left, int right){
int pivot = left;
int index = pivot + 1;
for (int i = left; i <= right; ++i){
if (input[i] < input[pivot]){
swap(input, i, index);
++index;
}
}
swap(input, pivot, index - 1);
return index - 1;
}
public void swap(int[] input, int a, int b){
int tmp = input[a];
input[a] = input[b];
input[b] = tmp;
}
}