【算法 排序】冒泡·插入·归并·快速·堆排序算法总结

排序算法总结及实现源码

  • 笔记
  • 实现源码

笔记

排序算法
		分类
			基本排序	[快速无Bug]
				冒泡
				插入
			高级排序	[必考]
				归并
				快速
				拓扑 Topological
			其他重点 [需要研究]
				堆排序
				桶排序

		冒泡排序 [稳定]
			耗时: O(n^2)
			空间: S(1)
			核心思想: 从头开始,相邻两两比较交换,直到尾部 
						=> 这一轮中最大/小元素 冒泡到 Array 尾部 [横着看 吐泡泡]
		选择排序 [非稳定]
			耗时: O(n^2/2)
			空间: S(1)
			和冒泡类似,但不需要频繁两两邻近赋值操作,只需要更新赋值当前最大/小 => 少量赋值

		插入排序 [稳定]
			耗时: O(n) / O(n^2/2)
			空间: S(1)
			核心思想: 从第二个元素开始,向前比较,[后部数组] 右/后移
						在原后部第一元素位置 覆盖 插入元素

		归并排序 [稳定]
			耗时: O(nlogn) / O(nlogn)
			空间: S(2n)
			核心思想: [分治] 将复杂问题拆分成若干的子问题,然后一一解决
						一直将子数组拆分成更小的子数组(2路=>2个子数组),直到数组中只有1个元素[先局部有序]
						然后再合并各个局部有序子数组 
						=> mergeSort(){
							if (hi <= lo) return;			// 递归终止条件
							int mid = lo + (hi - lo) / 2;	// 切分项
							mergeSort(左半)
							mergeSort(右半)
							merge()							// 合并排序
						}

		快速排序 [非稳]
			耗时: O(nlogn) / O(n^2/2)
			空间: S(nlogn)
			核心思想: [分治] 将数组 根据 切分项V 分为2块,不断交换左侧大于V 及 右侧小于V 的2个数组项i,j
						也就是粗排序,交换左右不合规元素 => 区域有序,然后再进行递归切分,实现更细区域有序
						最终,递归到底达成全局有序 [反着来的归并排序]
						=> quickSort(){
							if (hi <= lo) return;	// 递归终止条件
							partition()				// 粗排序+切分项
							quickSort(左半)
							quickSort(右半)
						}

			与归并区别: 
				同
					二者很像 [最好情况下将所有内容精确地分成两半。这使它有点像“归并排序”]
					满足归并排序——分而治之递归算法:Cn = 2Cn + n 公式 ==> N*lgN
				异
					1.Partition in-place 就地分区
						使用一个额外数组来更容易分区及实现Stable,但这不值得
						QS 更快 更节省空间,这是QS优势所在
					2.shuffleing 是必要的
						对于保证性能来说,失去会导致性能受损。切分项也可以随机
						随机性是为了平衡最坏情况下的性能 => 最坏情况下的概率保证
					3.排序与递归顺序
						QS [先粗排序再递归切分]
						MS [先递归切分再合并排序]
						区域性有序: 区域内 无序,但 均大于 x / 均小于 x
						局部性有序: 局部内 有序, 全局无序

		堆排序 [非稳]
			耗时: O(nlogn) / O(2nlogn + 2n)
			空间: S(n)

			用途: 在现实嵌入式较少资源情况下唯一使用的原地排序算法
					最重要的是它能在插入、删除 混合操作动态场景下能保持 对数级别的计算时间

			数据结构: 优先队列([二叉堆]实现(数组))
				二叉堆: [堆有序]的[完全[二叉树]]结构元素,在数组中按层级存储
				堆有序: 每个节点都大于等于它的两个子节点时,称为堆有序
				二叉树: 空子节点,或者连接指向左右子节点(也是一颗二叉树)
				完全二叉树: 完美平衡 => 如果树高 4,则一二三层均是完整左右子树,第四层可以不完美

				完全二叉树性质:
					1.完全二叉树可根据 节点数 计算 树高 [ N nodes => lgN ]
					2.只用数组而不需要指针就可以表示/实现

				二叉堆性质: 
					1.不使用数组索引0
					2.从任意节点向上,都能获取一列 非递减的元素
						从任意节点向下,都能获取一列 非递增的元素
					3.二叉堆中索引位置为 k 的元素的父亲为 k/2 向下取整,其子节点为 2k 及 2k+1
			核心思想: 
				构建 MaxPriorityQueue 二叉堆实现的优先队列[其实数组就行],并重复删除最大元素/堆顶元素 
					=> 即两阶段: 1.堆构造阶段(堆有序); 2.下沉阶段 (删除堆顶/输出最大元素)

			与快排归并区别
				异
					1.全局无序,条状有序(从下至上/从上至下一条线)
					2.通过不断 输出(delete/sink) 全局最大/小 至数组末尾 实现 排序

		拓扑排序 [顶点排序]
			前提: 必须有向图; 图里没有环; 没有孤岛 【统计前驱/入度; 广度优先搜索遍历;】
			耗时: O(n)
			空间: S(n)
			
			用途: 理清具有依赖关系的任务 [将图论的顶点按照相连的性质进行排序]

			void sort(){
				Queue q = new LinkedList();
				for(int v = 0; v < V; v++)
					if(indegree[v] == 0) q.add(v);
				while(!q.isEmpty()){
					int v = q.poll();
					print(v);

					for(int u = 0; u < adj[v].length; u++)
						if(--indegree[u] == 0) q.add(u);
				}
			}

		时间复杂度比较图
						inplace?	stable?	worst	average	best	remarks
			selection	x					N^2/2	N^2/2	N^2/2	N次交换
			insertion	x			x		N^2/2	N^2/4	N 		适用于小量(15)元素
			shell		x					?		?		N 		tight code, subquadratic
			quick 		x					N^2/2	2NlogN 	NlogN 	probabilistic guarantee fastest
			3-way quick x					N^2/2	2NlogN 	N 		针对重复Key提升
			merge 					x		NlogN 	NlogN 	NlogN 	稳定NlogN(2种含义)
			heap 		x					2NlogN 	2NlogN 	NlogN 	代码简单 插删混合对数级别

实现源码

/**
 * @Project Algorithm
 * @Author INBreeze
 * @Date 2020/3/28 9:51
 * @Description Sort
 */
@SuppressWarnings({"unused"})
public class Sort {
    /*******************************************测试函数*******************************************/
    public static void main(String[] args) {
        int[] a = {0, 5, 4, 6, 2, 1};           // 堆排序默认 不使用 a[0] 故此,在a[0]填充0元素
        //a = popSort(a);
        //a = insertSort1(a);
        //a = insertSort2(a);
        //mergeSortEntrance(a);
        //quickSortEntrance(a);
        //heapSort(a);
        for (int i : a) {
            System.out.println(i);
        }

        // a[0], a[1]
        /*int i = 0;
        System.out.println(a[i++]);
        System.out.println(a[++i]);*/
    }

    /*******************************************冒泡排序*******************************************/
    static int[] popSort(int[] arr) {
        // 总冒泡次数 是 n-1 [最后一个数不需要冒泡]
        // 内部比较次数 是 越来越少的 n-1,n-2,n-3,...,1·等差数组求和 n(a1+an)/2 [因为数组尾部有序]
        // 空间复杂度 S(1) [只需要 tmp 变量]
        // 时间复杂度 O( (n-1)(1+n-1)/2 = (n-1)n/2 = (n^2-n)/2 ≈ O(n^2) [n最够大时]
        // 最好情况: 直接有序 O(n)
        // 最坏情况: 完全倒序 O(n^2)
        // 理解: 只管最大泡泡冒出

        boolean hasChanged = true;
        for (int n = 1; n < arr.length && hasChanged; n++) {    // 总冒泡次数
            hasChanged = false;                                     // 优化1: 提前终止
            for (int i = 0; i < arr.length - n; i++) {          // 每次冒泡两两比较的次数
                int j = i + 1, tmp;
                if (arr[i] > arr[j]) {
                    tmp = arr[i];
                    arr[i] = arr[j];
                    arr[j] = tmp;
                    hasChanged = true;
                }
            }
        }
        return arr;
    }

    /*******************************************插入排序*******************************************/
    static int[] insertSort1(int[] arr) {
        // 空间复杂度 S(1) [只需要 current 变量]
        // 时间复杂度
        // 最好情况: 直接有序 O(n)
        // 最坏情况: 完全倒序 O(n^2)
        // 理解: 累积局部有序 后部数组右移 插入新值

        for (int i = 1; i < arr.length; i++) {                  // 新插入的值位置索引i
            int k = i;                                              // 优化1: 避免i左移后的重复比较 k用于保存当前i
            for (int j = i - 1; j >= 0; j--) {                  // 局部有序数组范围索引j
                if (arr[j] > arr[k]) {
                    int tmp = arr[k];
                    arr[k] = arr[j];
                    arr[j] = tmp;
                    k = j;                                  // i向左冒泡位置索引已改变 使用k保存变化的i
                }
            }
        }
        return arr;
    }

    static int[] insertSort2(int[] arr) {
        for (int i = 1, j, current; i < arr.length; i++) {
            current = arr[i];

            for (j = i - 1; j >= 0 && arr[j] > current; j--) {      // 优化2: 不进行冒泡交换,直接将 cur < [后部数组] 右/后移
                arr[j + 1] = arr[j];
            }

            arr[j + 1] = current;                                   // 当寻找到 [前部数组] > cur 进行 arr[j+1] <-覆盖- cur
        }
        return arr;
    }

    /*******************************************归并排序*******************************************/
    // 对外入口函数·迪米特法则
    static void mergeSortEntrance(int[] a) {
        int[] aux = new int[a.length]; // 辅助数组
        mergeSort(a, aux, 0, a.length - 1);    // 实际核心函数sort
    }

    // 实际核心函数sort
    static void mergeSort(int[] a, int[] aux, int lo, int hi) {
        if (hi <= lo) return;                   // 递归终止条件
        int mid = lo + (hi - lo) / 2;
        mergeSort(a, aux, lo, mid);             // 左半递归切分
        mergeSort(a, aux, mid + 1, hi);     // 右半递归切分
        merge(a, aux, lo, mid, hi);             // 合并排序 [左半右半] [先递归切分再合并排序]
    }

    static void merge(int[] a, int[] aux, int lo, int mid, int hi) {
        // assert isSorted(a, lo, mid);
        // assert isSorted(a, mid + 1, hi);
        // copy
        if (hi + 1 - lo >= 0) System.arraycopy(a, lo, aux, lo, hi + 1 - lo);
        //merge
        int i = lo, j = mid + 1;
        for (int k = lo; k <= hi; k++) {
            if (i > mid)                    // 1.左半已全部移入,将右半剩余移入 原地数组a[]
                a[k] = aux[j++];
            else if (j > hi)                // 2.右半已全部移入,将左半剩余移入 原地数组a[]
                a[k] = aux[i++];
            else if (less(aux[j], aux[i]))  // 3.右半目标小,则将右半移入 原地数组a[]
                a[k] = aux[j++];
            else                            // 4.左半目标小,则将左半移入 原地数组a[]
                a[k] = aux[i++];
        }
    }

    static boolean less(int a, int b) {
        return a < b;
    }

    /*******************************************快速排序*******************************************/
    static void quickSortEntrance(int[] a) {
        //StdRandom.shuffle(a);                         // Shuffle·消除输入依赖
        quick(a, 0, a.length - 1);
    }

    static void quick(int[] a, int lo, int hi) {
        if (hi <= lo) return;                                // 递归终止条件
        int j = partition(a, lo, hi);                   // Partition·寻找切分项 && 左右半区粗排序 [先粗排序再递归]
        quick(a, lo, j - 1);                            // Sort·左半(<切分项)递归
        quick(a, j + 1, hi);                            // Sort·右半(切分项<)递归
    }

    static int partition(int[] a, int lo, int hi) {
        int i = lo, j = hi + 1;                         // a[lo]是不需要比较作为切分项,但a[hi]需要比较故此先+1
        int v = a[lo];                                  // 一般假定a[lo]数组首项为 切分项v · 也可以进行随机选择
        while (true) {
            while (less(a[++i], v)) if (i == hi) break;    // 找到不小于 v 在左侧(相对于切分项v)
            while (less(v, a[--j])) if (j == lo) break;    // 找到不大于 v 在右侧(相对于切分项v)
            if (i >= j) break;                // 如果 i j 游标相遇,则不需要再查找
            exch(a, i, j);                    // 交换,左侧不小于v 与 右侧不大于v 的两个不合规数组项i,j
        }
        // j 最终会越过 i 指向 小于 v 的数组项
        exch(a, lo, j);                        // 最后交换,切分项v 至 正确位置j [a[j]之后全部都是>=v的]
        return j;
    }

    static void exch(int[] a, int left, int right) {
        int tmp = a[left];
        a[left] = a[right];
        a[right] = tmp;
    }

    /*******************************************堆排序*******************************************/
    static void heapSort(int[] a) {     // 注意: 堆排序不使用
        // 阶段1 堆构建
        int N = a.length-1;
        for (int k = N / 2; k >= 1; k--) {
            sink(a, k, N);
        }
        // 阶段2 下沉
        while (N > 1) {
            exch(a, 1, N);        // 交换 堆顶最大元素 与 最后元素 [这样堆排序就不需要额外数组空间]
            sink(a, 1, --N);        // 第三个参数为数组长度,因为是同一数组内原地排序,需要减少数组长度
        }
    }

    static void sink(int[] a, int k, int N) {
        while (2 * k <= N) {
            int j = 2 * k;                                  // j = 左子
            if (j < N && less(a[j], a[j + 1])) j++;         // 如果k位置有子节点 并且 左子<右子 则 j = 右子
            if (!less(a[k], a[j])) break;                   // 如果 k > 最大子节点 则不变
            exch(a, k, j);                                  // 交换 k 与其 最大子节点
            k = j;                                          // 继续下沉 k 对象
        }
    }
}

你可能感兴趣的:(算法)