十大排序演算法詳解

MPolaris發表於2021-03-15

1. 十大排序演算法

其中 冒泡,選擇,歸併,快速,希爾,堆排序屬於比較排序

20210130002554

穩定性理解

如果相等的兩個元素,在排序前後的相對位置保持不變,那麼這是穩定的排序演算法。

  • 排序前:5,1,3(a),4,7,3(b)

  • 穩定的排序:1,3(a),3(b),4,5,7

  • 不穩定的排序:1,3(b),3(a),4,5,7

原地演算法(In-place Algorithm)理解

定義:不依賴額外的資源或依賴少數的額外資源(空間複雜度較低),僅依靠輸出覆蓋輸入(例如直接對輸入的陣列進行操作)

2. 工具類

用於提供測試資料與測試程式碼正確性

2.1 斷言工具類
public class Asserts {
   public static void test(boolean value) {
      try {
         if (!value) throw new Exception("測試未通過");
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}
2.2 Integers工具類
public class Integers {
	/** 生成隨機數 */
	public static Integer[] random(int count, int min, int max) {
		if (count <= 0 || min > max) return null;
		Integer[] array = new Integer[count];
		int delta = max - min + 1;
		for (int i = 0; i < count; i++) {
			array[i] = min + (int)(Math.random() * delta);
		}
		return array;
	}

	/** 合併兩個陣列 */
	public static Integer[] combine(Integer[] array1, Integer[] array2) {
		if (array1 == null || array2 == null) return null;
		Integer[] array = new Integer[array1.length + array2.length];
		for (int i = 0; i < array1.length; i++) {
			array[i] = array1[i];
		}
		for (int i = 0; i < array2.length; i++) {
			array[i + array1.length] = array2[i];
		}
		return array;
		
	}

	public static Integer[] same(int count, int unsameCount) {
		if (count <= 0 || unsameCount > count) return null;
		Integer[] array = new Integer[count];
		for (int i = 0; i < unsameCount; i++) {
			array[i] = unsameCount - i;
		}
		for (int i = unsameCount; i < count; i++) {
			array[i] = unsameCount + 1;
		}
		return array;
	}

	/**
	 * 生成頭部和尾部是升序的陣列
	 * disorderCount:希望多少個資料是無序的
	 */
	public static Integer[] headTailAscOrder(int min, int max, int disorderCount) {
		Integer[] array = ascOrder(min, max);
		if (disorderCount > array.length) return array;
		
		int begin = (array.length - disorderCount) >> 1;
		reverse(array, begin, begin + disorderCount);
		return array;
	}

	/**
	 * 生成中間是升序的陣列
	 * disorderCount:希望多少個資料是無序的
	 */
	public static Integer[] centerAscOrder(int min, int max, int disorderCount) {
		Integer[] array = ascOrder(min, max);
		if (disorderCount > array.length) return array;
		int left = disorderCount >> 1;
		reverse(array, 0, left);
		
		int right = disorderCount - left;
		reverse(array, array.length - right, array.length);
		return array;
	}

	/**
	 * 生成頭部是升序的陣列
	 * disorderCount:希望多少個資料是無序的
	 */
	public static Integer[] headAscOrder(int min, int max, int disorderCount) {
		Integer[] array = ascOrder(min, max);
		if (disorderCount > array.length) return array;
		reverse(array, array.length - disorderCount, array.length);
		return array;
	}

	/**
	 * 生成尾部是升序的陣列
	 * disorderCount:希望多少個資料是無序的
	 */
	public static Integer[] tailAscOrder(int min, int max, int disorderCount) {
		Integer[] array = ascOrder(min, max);
		if (disorderCount > array.length) return array;
		reverse(array, 0, disorderCount);
		return array;
	}

	/** 升序生成陣列 */
	public static Integer[] ascOrder(int min, int max) {
		if (min > max) return null;
		Integer[] array = new Integer[max - min + 1];
		for (int i = 0; i < array.length; i++) {
			array[i] = min++;
		}
		return array;
	}

	/** 降序生成陣列 */
	public static Integer[] descOrder(int min, int max) {
		if (min > max) return null;
		Integer[] array = new Integer[max - min + 1];
		for (int i = 0; i < array.length; i++) {
			array[i] = max--;
		}
		return array;
	}
	
	/** 反轉陣列 */
	private static void reverse(Integer[] array, int begin, int end) {
		int count = (end - begin) >> 1;
		int sum = begin + end - 1;
		for (int i = begin; i < begin + count; i++) {
			int j = sum - i;
			int tmp = array[i];
			array[i] = array[j];
			array[j] = tmp;
		}
	}

	/** 複製陣列 */
	public static Integer[] copy(Integer[] array) {
		return Arrays.copyOf(array, array.length);
	}

	/** 判斷陣列是否升序 */
	public static boolean isAscOrder(Integer[] array) {
		if (array == null || array.length == 0) return false;
		for (int i = 1; i < array.length; i++) {
			if (array[i - 1] > array[i]) return false;
		}
		return true;
	}

	/** 列印陣列 */
	public static void println(Integer[] array) {
		if (array == null) return;
		StringBuilder string = new StringBuilder();
		for (int i = 0; i < array.length; i++) {
			if (i != 0) string.append("_");
			string.append(array[i]);
		}
		System.out.println(string);
	}
}
2.3 時間測試工具類
public class Times {
	private static final SimpleDateFormat fmt = new SimpleDateFormat("HH:mm:ss.SSS");
	
	public interface Task {
		void execute();
	}
	
	public static void test(String title, Task task) {
		if (task == null) return;
		title = (title == null) ? "" : ("【" + title + "】");
		System.out.println(title);
		System.out.println("開始:" + fmt.format(new Date()));
		long begin = System.currentTimeMillis();
		task.execute();
		long end = System.currentTimeMillis();
		System.out.println("結束:" + fmt.format(new Date()));
		double delta = (end - begin) / 1000.0;
		System.out.println("耗時:" + delta + "秒");
		System.out.println("-------------------------------------");
	}
}
2.4 Sort抽象父類
public abstract class Sort<T extends Comparable<T>> implements Comparable<Sort<T>> {
    /** 目標陣列 */
    protected T[] array;
    /** 比較次數 */
    private int cmpCount;
    /** 交換次數 */
    private int swapCount;
    /** 執行時間 */
    private long time;
    /** 小數格式化 */
    private DecimalFormat fmt = new DecimalFormat("#.00");

    /** 預處理 */
    public void sort(T[] array) {
        if (array == null || array.length < 2) return;
        this.array = array;
        long begin = System.currentTimeMillis();
        sort();
        time = System.currentTimeMillis() - begin;
    }

    /** 目標方法 */
    protected abstract void sort();

    /**
     * 比較陣列下標對應的值
     *
     * 返回值等於0,代表 array[index1] == array[index2]
     * 返回值小於0,代表 array[index1] < array[index2]
     * 返回值大於0,代表 array[index1] > array[index2]
     */
    protected int cmp(int index1, int index2) {
        cmpCount++;
        return array[index1].compareTo(array[index2]);
    }

    /** 比較值 */
    protected int cmp(T value1, T value2) {
        cmpCount++;
        return value1.compareTo(value2);
    }

    /** 交換值 */
    protected void swap(int index1, int index2) {
        swapCount++;
        T tmp = array[index1];
        array[index1] = array[index2];
        array[index2] = tmp;
    }

    /** 穩定性測試 */
    @SuppressWarnings("unchecked")
    private boolean isStable() {
        Student[] students = new Sort.Student[20];
        for (int i = 0; i < students.length; i++) {
            //(0,10) (10,10) (20,10) (30,10)
            students[i] = new Student(i * 10, 10);
        }
        sort((T[]) students);//只會對年齡進行排序
        for (int i = 1; i < students.length; i++) {
            int score = students[i].score;
            int prevScore = students[i - 1].score;
            if (score != prevScore + 10) return false;
        }
        return true;
    }

    private static class Student implements Comparable<Student>{
        Integer score;
        Integer age;
        public Student(Integer score, Integer age) {
            this.score = score;
            this.age = age;
        }

        @Override
        public int compareTo(Student o) {
            return age - o.age;
        }
    }

    /** 排序方式 */
    @Override
    public int compareTo(Sort o) {
        int result = (int)(time - o.time);
        if(result != 0) return result;
        result = cmpCount - o.cmpCount;
        if(result != 0) return result;
        return swapCount - o.swapCount;
    }

    @Override
    public String toString() {
        return "【" + getClass().getSimpleName() + "】\n"
                + "交換次數 ==> " + numberString(swapCount) + "\n"
                + "比較次數 ==> " + numberString(cmpCount) + "\n"
                + "執行時間 ==> " + time * 0.001 + "s" + "\n"
                + "穩定性 ==> " + isStable() + "\n"
                + "=================================";
    }

    /** 數字格式化 */
    private String numberString(int number) {
        if (number < 10000) return "" + number;

        if (number < 100000000) {
            return fmt.format(number / 10000.0) + "萬";
        }
        return fmt.format(number / 100000000.0) + "億";
    }

}

3. 氣泡排序(Bubble Sort)

3.1 執行流程
  • 從頭開始比較每一對相鄰元素,如果第一個比第二個大就交換它們的位置。執行完一輪後最末尾哪個元素就是最大的元素
  • 忽略第一步找到的最大元素,重複執行第一步,直到全部元素有序
BubbleSort
3.2 基本實現
public void sort() {
    for (int eIndex = array.length - 1; eIndex > 0; eIndex--) {
        for (int i = 1; i <= eIndex; i++) {
            if (cmp(i, i - 1) < 0) {
                swap(i, i - 1);
            }
        }
    }
}
3.4 優化一

優化方案:如果序列已經完全有序,可以提前終止氣泡排序

缺點:只有當完全有序時才會提前終止氣泡排序,概率很低

public void sort() {
    for (int eIndex = array.length - 1; eIndex > 0; eIndex--) {
        boolean sorted = true;
        for (int i = 1; i <= eIndex; i++) {
            if (cmp(i,i - 1) < 0) {
                swap(i, i - 1);
                sorted = false;
            }
        }
        if (sorted) break;
    }
}
3.5 優化二

優化方案:如果序列尾部已經區域性有序,可以記錄最後一次交換的位置,減少比較次數

20210130011659
public class BubbleSort<T extends Comparable<T>> extends Sort<T> {
    /**
     *  優化方式二:如果序列尾部已經區域性有序,可以記錄最後依次交換的位置,減少比較次數
     *  為什麼這裡sortedIndex為1(只要保證 eIndex-- > 0 即可)?
     *     => 如果sortedIndex為eIndex,當陣列第一次就完全有序時,就退回到最初的版本了
     *     => 如果sortedIndex為1,當陣列第一次就完全有序時,一輪掃描就結束了!
     * 
     */
    @Override
    public void sort() {
        for (int eIndex = array.length - 1; eIndex > 0; eIndex--) {
            int sortedIndex = 1; //記錄最後一次交換的下標位置
            for (int i = 1; i <= eIndex; i++) {
                if (cmp(i, i - 1) < 0) {
                    swap(i, i - 1);
                    sortedIndex = i;
                }
            }
            eIndex = sortedIndex;
        }
    }
}
3.6 演算法優劣
  • 最壞,平均時間複雜度:O(n^2),最好時間複雜度:O(n)

  • 空間複雜度:O(1)

  • 屬於穩定排序

注意:稍有不慎,穩定的排序演算法也能被寫成不穩定的排序演算法,如下氣泡排序是不穩定的

public void sort() {
    for (int eIndex = array.length - 1; eIndex > 0; eIndex--) {
        for (int i = 1; i <= eIndex; i++) {
            if (cmp(i, i - 1) <= 0) {
                swap(i, i - 1);
            }
        }
    }
}
  • 屬於原地演算法

4. 選擇排序(Selection Sort)

4.1 執行流程
  • 從序列中找出最大的哪個元素,然後與最末尾的元素交換位置。執行完一輪後最末尾那個元素就是最大的元素
  • 忽略第一步找到的最大元素,重複執行第一步

這裡以選最小元素為例

SelectionSort
4.2 基本實現
public class SelectionSort<T extends Comparable<T>> extends Sort<T> {
    @Override
    public void sort() {
        for (int eIndex = array.length - 1; eIndex > 0; eIndex--) {
            int maxIndex = 0;
            for (int i = 1; i <= eIndex; i++) {
                //注意:為了穩定性,這裡要寫 <=
                if (cmp(maxIndex, i) <= 0) {
                    maxIndex = i;
                }
            }
            if(maxIndex != eIndex) swap(maxIndex, eIndex);
        }
    }

}
4.3 演算法優劣
  • 選擇排序的交換次數要遠少於氣泡排序,平均效能優於氣泡排序
  • 最好,最壞,平均時間複雜度均為O(n^2),空間複雜度為O(1),屬於不穩定排序

選擇排序是否還有優化的空間? => 使用堆來選擇最大值

5. 堆排序(Heap Sort)

堆排序可以認為是對選擇排序的一種優化

5.1 執行流程
  • 對序列進行原地建堆(heapify)
  • 重複執行以下操作,直到堆的元素數量為1
    • 交換堆頂元素與尾元素
    • 堆的元素數量減1
    • 對0位置進行一次siftDown操作
image-20210130235449796
5.2 基本實現
public class HeapSort<T extends Comparable<T>> extends Sort<T> {
    /** 記錄堆資料 */
    private int heapSize;

    @Override
    protected void sort() {
        // 原地建堆(直接使用陣列建堆)
        heapSize = array.length;
        for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
            siftDown(i);
        }
        while (heapSize > 1) {
            // 交換堆頂元素和尾部元素
            swap(0, --heapSize);

            // 對0位置進行siftDown(恢復堆的性質)
            siftDown(0);
        }
    }

    /** 堆化 */
    private void siftDown(int index) {
        T element = array[index];

        int half = heapSize >> 1;
        while (index < half) { // index必須是非葉子節點
            // 預設是左邊跟父節點比
            int childIndex = (index << 1) + 1;
            T child = array[childIndex];

            int rightIndex = childIndex + 1;
            // 右子節點比左子節點大
            if (rightIndex < heapSize &&
                    cmp(array[rightIndex], child) > 0) {
                child = array[childIndex = rightIndex];
            }

            // 大於等於子節點
            if (cmp(element, child) >= 0) break;

            array[index] = child;
            index = childIndex;
        }
        array[index] = element;
    }
}
5.2 演算法優劣
  • 最好,最壞,平均時間複雜度:O(nlog^n)

  • 空間複雜度:O(1)

  • 屬於不穩定排序

5.3. 冒泡,選擇,堆排序比較
@SuppressWarnings({"rawtypes","unchecked"})
public class SortTest {
    public static void main(String[] args) {
        Integer[] arr1 = Integers.random(10000, 1, 20000);
        testSort(arr1,
                new SelectionSort(),
                new HeapSort(),
                new BubbleSort());

    }

    static void testSort(Integer[] arr,Sort... sorts) {
        for (Sort sort: sorts) {
            Integer[] newArr = Integers.copy(arr);
            sort.sort(newArr);
            //檢查排序正確性
            Asserts.test(Integers.isAscOrder(newArr));
        }
        Arrays.sort(sorts);
        for (Sort sort: sorts) {
            System.out.println(sort);
        }
    }
}
image-20210130235941183

6. 插入排序(Insertion Sort)

6.1 執行流程
  • 在執行過程中,插入排序會將序列分為兩部分(頭部是已經排好序的,尾部是待排序的)

  • 從頭開始掃描每一個元素,每當掃描到一個元素,就將它插入到頭部適合的位置,使得頭部資料依然保持有序

InsertionSort
6.2 基本實現
public class InsertionSort<T extends Comparable<T>> extends Sort<T> {
    @Override
    protected void sort() {
        for (int i = 1; i < array.length; i++) {
            int cur = i;
            while(cur > 0 && cmp(cur,cur - 1) < 0) {
                swap(cur,cur - 1);
                cur--;
            }
        }
    }
}
6.3 逆序對(Inversion)

什麼是逆序對? => 陣列 [2,3,8,6,1] 的逆序對為:<2,1> < 3,1> <8,1> <8,6> <6,1>

插入排序的時間複雜度與逆序對的數量成正比關係

時間複雜度最高如下:O(n^2)

image-20210131010515436
6.4 優化一

優化思路 => 將交換改為挪動

  • 先將待插入元素備份

  • 頭部有序資料中比待插入元素大的,都朝尾部方向挪動1個位置

  • 將待插入元素放到最終合適位置

注意:逆序對越多,該優化越明顯

image-20210131012202402
public class InsertionSort<T extends Comparable<T>> extends Sort<T> {
    @Override
    protected void sort() {
        for (int i = 1; i < array.length; i++) {
            int cur = i;
            T val = array[cur];
            while(cur > 0 && cmp(val,array[cur - 1]) < 0) {
                array[cur] = array[cur - 1];//優化重點在這裡
                cur--;
            }
            array[cur] = val;
        }
    }
}
6.5 優化二

優化思路 => 將交換改為二分搜尋(較少比較次數)

二分搜尋理解

如何確定一個元素在陣列中的位置?(假設陣列裡全是整數)

  • 如果是無序陣列,從第 0 個位置開始遍歷搜尋,平均時間複雜度:O(n)

  • 如果是有序陣列,可以使用二分搜尋,最壞時間複雜度:O(log^n)

思路

  • 如下,假設在 [begin, end) 範圍內搜尋某個元素 v,mid == (begin + end) / 2
  • 如果 v < mid,去 [begin,mid) 範圍內二分搜尋
  • 如果 v > mid,去 [mid + 1,end) 範圍內二分搜尋
  • 如果 v == mid,直接返回 mid
image-20210131214722123

例項

image-20210131215305715
/** 二分搜尋-基本實現
 *      查詢val在有序陣列arr中的位置,找不到就返回-1
 */
private static int indexOf(Integer[] arr,int val) {
    if(arr == null || arr.length == 0) return -1;
    int begin = 0;
    //注意這裡end設計為arr.length便於求數量(end - begin)
    int end = arr.length;
    while (begin < end) {
        int mid = (begin + end) >> 1;
        if(val < arr[mid]) {
            end = mid;
        } else if(val > arr[mid]) {
            begin = mid  + 1;
        } else {
            return mid;
        }
    }
    return -1;
}

二分搜尋(Binary Search)優化實現

  • 之前的插入排序程式碼,在元素 val 的插入過程中,可以先二分搜尋出合適的插入位置,然後將元素 val 插入
  • 適合於插入排序的二分搜尋必須滿足:要求二分搜尋返回的插入位置是第1個大於 val 的元素位置
    • 如果 val 是 5 ,返回 2
    • 如果 val 是 1,返回 0
    • 如果 val 是15,返回 7
    • 如果 val 是 8,返回 5
image-20210131221938281
  • 實現思路
    • 假設在 [begin,end) 範圍內搜尋某個元素 val,mid == (begin + end) / 2
    • 如果val < mid,去 [begin,mid) 範圍內二分搜尋
    • 如果val >= mid,去 [mid + 1,end) 範圍內二分搜尋
    • 當 begin == end == x,x 就是待插入位置
  • 例項
image-20210131224559325
/**
 * 二分搜尋-適用於插入排序
 *    查詢val在有序陣列arr中可以插入的位置
 *    規定:要求二分搜尋返回的插入位置是第1個大於 val 的元素位置
 */
private static int search(Integer[] arr,int val) {
    if(arr == null || arr.length == 0) return -1;
    int begin = 0;
    int end = arr.length;
    while (begin < end) {
        int mid = (begin + end) >> 1;
        if(val < arr[mid]) {
            end = mid;
        } else {
            begin = mid  + 1;
        }
    }
    return begin;
}

插入排序最終實現

注意:使用了二分搜尋後,只是減少了比較次數,但插入排序的平均時間複雜度依然是O(n^2)

public class InsertionSort<T extends Comparable<T>> extends Sort<T> {
 
    /** 優化 => 二分搜尋 */
    @Override
    protected void sort() {
        for (int begin = 1; begin < array.length; begin++) {
            //這裡為什麼傳索引而不是傳值?
            // => 傳索引還可以知道前面已經排好序的陣列區間:[0,i)
            insert(begin,search(begin));
        }
     }

    /** 將source位置的元素插入到dest位置 */
    private void insert(int source,int dest) {
         //將[dest,source)範圍內的元素往右邊挪動一位
         T val = array[source];
         for (int i = source; i > dest; i--) {
             array[i] = array[i - 1];
         }
         //插入
         array[dest] = val;
    }

    private int search(int index) {
        T val = array[index];
        int begin = 0;
        int end = index;
        while (begin < end) {
            int mid = (begin + end) >> 1;
            if(cmp(val,array[mid]) < 0) {
                end = mid;
            } else {
                begin = mid  + 1;
            }
        }
        return begin;
    }
}
6.6 演算法優劣
image-20210131231517195
  • 最壞,平均時間複雜度為 O(n^2),最好時間複雜度為 O(n)
  • 空間複雜度為 O(1)
  • 屬於穩定排序

7. 歸併排序(Merge Sort)

7.1 執行流程
  • 不斷的將當前序列平均分割成 2 個子序列,直到不能再分割(序列中只剩一個元素)
  • 不斷的將 2 個子序列合併成一個有序序列,直到最終只剩下 1 個有序序列

MergeSortimage-20210131234043161

7.2 思路

merge

大致想法

image-20210201001248209

細節

  • 需要 merge 的 2 組序列存在於同一個陣列中,並且是挨在一起的
image-20210201001738365
  • 為了更好的完成 merge 操作,最好將其中 1 組序列備份出來,比如 [begin,mid)
image-20210201002214497
  • 基本實現
image-20210201002810524
  • 情況一:左邊先結束 => 左邊一結束整個歸併就結束
image-20210201003223422
  • 情況二:右邊先結束 => 右邊一結束就直接將左邊按順序挪到右邊去
image-20210201003620035
7.3 基本實現
@SuppressWarnings("unchecked")
public class MergeSort<T extends Comparable<T>> extends Sort<T> {
    private T[] leftArr;

    @Override
    protected void sort() {
        leftArr = (T[]) new Comparable[array.length >> 1];
        sort(0, array.length);
    }

    /** 對 [begin,end) 位置的元素進行歸併排序 */

    private void sort(int begin, int end) {
        if (end - begin < 2) return;
        int mid = (begin + end) >> 1;
        sort(begin, mid);
        sort(mid, end);
        merge(begin, mid, end);
    }

    /** 將 [begin,mid) 和 [mid,end) 範圍的序列合併成一個有序序列 */
    private void merge(int begin, int mid, int end) {
        int li = 0, le = mid - begin;
        int ri = mid, re = end;
        int ai = begin;

        //備份左邊陣列
        for (int i = 0; i < le; i++) {
            leftArr[i] = array[begin + i];
        }

        //如果左邊還沒有結束(情況一)
        while (li < le) {
            //當 ri < re 不成立,就會一直leftArr挪動(情況二)
            if (ri < re && cmp(array[ri],leftArr[li]) < 0) {
                array[ai++] = array[ri++];
            } else { //注意穩定性
                array[ai++] = leftArr[li++];
            }
        }
    }
}
7.4 演算法優劣
image-20210201013107391

複雜度分析

T(n) = sort() + sort() + merge()
=> T(n) = T(n/2) + T(n/2) + O(n)
=>  T(n) = 2T(n/2) + O(n)
    
//由於sort()是遞迴呼叫,用T表示,由於T(n/2)不好估算,現在要理清T(n)與O(n)之間的關係
T(1) = O(1)
T(n)/n = T(n/2) / (n/2) + O(1)
    
//令S(n) = T(n)/n     
S(1) = O(1) 
S(n) = S(n/2) + O(1) 
     = S(n/4) + O(2)
     = S(n/8) + O(3)
     = S( n/(2^k) ) + O(k)
     = S(1) + O(log^n)
     = O(lon^n)
T(n) = n*S(n) = O(nlog^n)
    

=> 歸併排序時間複雜度:O(nlog^n)

常見遞推式

image-20210201013429126

總結

  • 由於歸併排序總是平均分割子序列,所以最好,最壞,平均時間複雜度都是:O(nlog^n)

  • 空間複雜度:O(n/2 + log^n) = O(n),n/2用於臨時存放左側陣列,log^n用於遞迴呼叫

  • 屬於穩定排序

相關文章