排序算法总结

1. 冒泡排序

原理:数组元素两两比较,交换位置,大元素往后放,经过一轮比较后,最大的元素就会出现在最大索引处 ( nums[].length-1-i )。

Java代码:

import java.util.Arrays;

public class Sort01 {
    // 冒泡排序
    public static void main(String[] args) {
        int[] nums = new int[]{11, 2, 43, 16, 30, 18, 22, 8};
        sort(nums);
        System.out.println(Arrays.toString(nums));
    }

    private static void sort(int[] nums) {
        // 由小到大排序
        for (int i = 0; i < nums.length; i++) {
            for (int j = 0; j < nums.length - 1 - i; j++) {
                // - i 的目的是除去动态比较最后一个元素的操作
                if (nums[j] > nums[j + 1]) {
                    swapValue(nums, j, j + 1);
                }
            }
        }
    }

    public static void swapValue(int[] nums, int i, int j) {
        int temp = nums[j];
        nums[j] = nums[i];
        nums[i] = temp;
    }
}

冒泡排序 时间复杂度为:O(½·n(n-1)) = O(n²)  /*(1+n-1 = n)*/


 

2. 选择排序

原理:从0索引处开始,依次和后面的元素进行比较,小的元素往前放,经过一轮比较后,最小的元素就出现在了最小索引处。

/* 选择排序是固定一个元素去向后遍历比较其他元素,而冒泡排序不同,是两两相邻元素的比较,类似双指针的移动窗口 */

Java代码:

import java.util.Arrays;

public class Sort02 {
    // 选择排序
    public static void main(String[] args) {
        int[] nums = new int[]{11, 2, 43, 16, 30, 18, 22, 8};
        sort(nums);
        System.out.println(Arrays.toString(nums));
    }

    private static void sort(int[] nums) {
        // 由小到大排序
        for (int i = 0; i < nums.length - 1; i++) {
            // j 起点是 第 i 个元素的下一个元素,因此需要保证 i 的最终循环值为 nums.length-2 < nums.length-1
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[i] > nums[j]) {
                    swapValue(nums, i, j);
                }
            }
        }
    }

    public static void swapValue(int[] nums, int i, int j) {
        int temp = nums[j];
        nums[j] = nums[i];
        nums[i] = temp;
    }
}

选择排序 时间复杂度为:O(½·n(n-1)) = O(n²) 


 

3. 直接插入排序

原理:将一个记录插入到一个长度为 nums.length 的有序列表中,使之仍保持有序。

/* 每次循环将待插入元素依次向前一个元素进行比较,符合大小关系判断条件则交换 */

Java代码:

import java.util.Arrays;

public class Sort03 {
    // 直接插入排序
    public static void main(String[] args) {
        int[] nums = new int[]{11, 2, 43, 16, 30, 18, 22, 8};
        sort(nums);
        System.out.println(Arrays.toString(nums));
    }

    private static void sort(int[] nums) {
        // 由小到大排序
        for (int i = 1; i < nums.length; i++) {
            // 从第二个元素开始往前插入即可,也保证后续地 while 循环内 j-1 不会导致数组越界
            int j = i;  // 防止遍历时 i-- 与 i++ 矛盾,引入另一个指针 j
            while (j > 0 && nums[j - 1] > nums[j]) {
                swapValue(nums, j, j - 1);
                j--;
            }
        }
    }

    public static void swapValue(int[] nums, int i, int j) {
        int temp = nums[j];
        nums[j] = nums[i];
        nums[i] = temp;
    }
}

直接插入排序 时间复杂度为:O(½·n(n-1)) = O(n²) 


 

4. 希尔 (Shell) 排序(缩小增量排序)—— 直接插入排序的优化(直接插入排序就是增量为1的希尔排序)

原理:先将原列表按增量(步长) h 分组,每组按照直接插入法排序。同样,用下一个增量 h/2 将列表再分组,再直接插入法排序。直到 h = 1 时,整个列表排序完成。每次分组排序的意义在于让列表总体上趋于有序。

关键:选择合适的增量 ht。(根据克努特序列寻找最优初始步长的方案 > 直接将初始步长设为数组长度/2的方案)

Java代码:

import java.util.Arrays;

public class Sort04 {
    // 希尔排序
    public static void main(String[] args) {
        int[] nums = new int[]{11, 2, 43, 16, 30, 18, 22, 8};
        sort(nums);
        System.out.println(Arrays.toString(nums));

    }

    private static void sort(int[] nums) {
        // 由小到大排序
        // 定义克努特序列的最大初始间隔 ——— 选取合适增量
        int gap = 1;
        while (gap <= nums.length / 3) {
            gap = gap * 3 + 1;
        }
        // 对每次增量进行形式上的直接插入排序
        for (int h = gap; h != 0; h = (h - 1) / 3) {
            // 1/2 = 0,即步长为 1 的循环结束后,跳出总循环
            for (int i = h; i < nums.length; i++) {
                for (int j = i; j > h - 1; j -= h) {
                    if (nums[j - h] > nums[j]) {
                        swapValue(nums, j, j - h);
                    }
                }
            }
        }
    }

    public static void swapValue(int[] nums, int i, int j) {
        int temp = nums[j];
        nums[j] = nums[i];
        nums[i] = temp;
    }
}

希尔排序 时间复杂度为: O(nˆ3/2


 

5. 快速排序(最好,但需要额外开辟栈空间 + 递归)

原理:(双指针依次交替遍历数组,对比中心轴小或大等于的数进行移位或不移位操作)

  思想:分治法(比大小,再分区,重复操作)

  1. 从数组中取一个数,作为基准数(中心轴);

  2. 分区:将比这个基准数大或者等于的数全放到它的右边,比这个数小的数全放在他的左边;

  3. 再对左右子序列重复第二步,直至序列只有一个数(while循环)。

  具体操作:挖坑填数

  1. 将基准数挖出形成第一个坑;

  2. 右指针由后向前找比基准数小的数,找不到则右指针--,找到则挖出此数填到前一个坑中,并将左指针++;

  3. 左指针由前向后找比基准数大或等于的数,找不到则左指针++,找到则挖出此数填到前一个坑中,并将右指针--;

  4. 重复执行2、3操作,直到做右指针重合(while循环)。

Java代码:

import java.util.Arrays;

public class Sort05 {
    // 快速排序
    public static void main(String[] args) {
        int[] nums = new int[]{11, 2, 43, 16, 30, 18, 22, 8};
        quickSort(nums, 0, nums.length - 1);
        System.out.println(Arrays.toString(nums));
    }

    private static void quickSort(int[] nums, int left, int right) {
        if (left < right) {
            int pivotIndex = getPivot(nums, left, right);
            //  分治(递归)+ 左右指针
            quickSort(nums, left, pivotIndex - 1);
            quickSort(nums, pivotIndex + 1, right);
        }
    }

    private static int getPivot(int[] nums, int left, int right) {
        // 将第一个元素设为基准值
        int pivotVal = nums[left];
        // 外循环结束的判断条件是左右指针重合
        while (left < right) {
            // 内循环_1 -- 右指针从右向左寻找比基准值小的数,若 nums[right]大或等于 pivotVal,right--;
            // *注意* 内层循环也要要求 left < right 防止 right--到小于 left或 left++到大于 right,从而导致 pivotIndex错位
            while (left < right && nums[right] >= pivotVal) {
                right--;
            }
            // 找到后跳出循环,做将 nums[right]值填到上一个坑的操作
            if (left < right) {
                // if 同样防止 pivotIndex无位置
                nums[left] = nums[right];
                left++;
            }

            // 内循环_2 同上(先右指针遍历再左指针遍历,顺序可变)
            while (left < right && nums[left] < pivotVal) {
                left++;
            }
            // 同上
            if (left < right) {
                nums[right] = nums[left];
                right--;
            }
        }
        // 左右指针重合之后,将 pivotVal的值赋给当前坐标位置的坑,返回 pivotIndex
        int pivotIndex = left;
        nums[pivotIndex] = pivotVal;
        return pivotIndex;
    }
}

快速排序 时间复杂度为: O(nlog2n);空间复杂度为O(log2n)


 

6. 归并排序(最稳定)

原理:利用归并的思想实现排序 —— 假设初始序列有 N 个记录,则可以看成是 N 个有序的子序列,每个子序列的长度为 1,然后两两归并,得到 N/2 个长度为 2 或 1 的有序子序列,再两两归并...,如此重复,直至得到一个长度为 N 的有序序列为止。这种排序也叫 二路归并排序。

Java代码:

import java.util.Arrays;

public class Sort06 {
    // 归并排序
    public static void main(String[] args) {
        int[] nums = new int[]{11, 2, 43, 16, 30, 18, 22, 8};
        sort(nums);
        System.out.println(Arrays.toString(nums));
    }

    private static void sort(int[] nums) {
        // 拆分入口
        split(nums, 0, nums.length - 1);
    }

    private static void split(int[] nums, int start, int end) {
        int center = (start + end) / 2;
        if (start < end) {
            // 1.先拆分 (递归调用 -- 二叉树后序遍历)
            split(nums, start, center);
            split(nums, center + 1, end);
            // 2.后归并
            merge(nums, start, center, end);
        }
    }

    private static void merge(int[] nums, int start, int center, int end) {
        // 临时数组 和 其初始位置 p
        int[] temp = new int[end - start + 1];
        int p = 0;
        // 左序列起始位置
        int leftStart = start;
        // 右序列起始位置
        int rightStart = center + 1;
        // 双指针动态比较两个数组的元素大小,将较小元素插入到 p指针位置,p++;同时被插入元素所在序列的的指针++
        while (leftStart <= center && rightStart <= end) {
            if (nums[leftStart] < nums[rightStart]) {
                temp[p] = nums[leftStart];
                leftStart++;
            } else {
                temp[p] = nums[rightStart];
                rightStart++;
            }
            p++;
        }
        // 处理较长的序列剩余元素(未比较部分),接到临时数组后面
        while (leftStart <= center) {
            // 左序列有剩余
            temp[p] = nums[leftStart++];
            p++;
        }
        while (rightStart <= end) {
            // 右序列有剩余
            temp[p] = nums[rightStart++];
            p++;
        }
        // Copy临时数组 temp[]到 nums[]
        for (int i = 0; i < temp.length; i++) {
            // i + start 是指两个子序列归并前最小的 nums[]位置
            nums[i + start] = temp[i];
        }
    }
}

归并排序 时间复杂度为: O(nlogn) ;空间复杂度为O(log2n)

改进归并排序 在归并时先判断前段序列的最大值与后段序列最小值的关系再确定是否进行复制比较。如果前段序列的最大值小于等于后段序列最小值,则说明序列可以直接形成一段有序序列不需要再归并,反之则需要。所以在序列本身有序的情况下时间复杂度可以降至O(n)。
 
TimSort 可以说是归并排序的终极优化版本,主要思想就是检测序列中的天然有序子段(若检测到严格降序子段则翻转序列为升序子段)。在最好情况下无论升序还是降序都可以使时间复杂度降至为O(n),具有很强的自适应性。

7. 基数排序(仅支持非负整数)

原理:不需要对关键字进行大小的比较,只需要根据关键字的每个位上的值进行 “分配” 和 “收集” 两种操作即可

Java代码:

import java.util.Arrays;

public class Sort07 {
    // 基数排序
    public static void main(String[] args) {
        int[] nums = new int[]{11, 2, 43, 16, 240, 181, 30, 18, 22, 8}; // 仅支持非负整数
        sort(nums);
        System.out.println(Arrays.toString(nums));
    }

    private static void sort(int[] nums) {
        // 定义二维数组来放 10个桶(栈)
        int[][] buckets = new int[10][nums.length];
        // 定义位数遍历次数的统计数组
        int[] count = new int[10];
        // 最大值
        int max = findMax(nums);
        // 最大值的位个数
        int len = String.valueOf(max).length();
        // 外循环 -- 位个数(n递乘10来获取每个元素的第n位上的数值)
        for (int i = 0, n = 1; i < len; i++, n *= 10) {
            // 内循环_1 -- 遍历原数组,找每个元素的第n位上的数值
            for (int j = 0; j < nums.length; j++) {
                // 获取每个位上的数
                int val = nums[j] / n % 10;
                buckets[val][count[val]++] = nums[j];
            }
            // 原数组初始下标
            int p = 0;
            // 内循环_1 -- 取出桶中的元素,至于原数组
            for (int x = 0; x < count.length; x++) {
                if (count[x] != 0) {
                    for (int y = 0; y < count[x]; y++) {
                        // 从桶中取出元素放至原数组中
                        nums[p++] = buckets[x][y];
                    }
                    // 清除一次外循环的各位数[0-9]的统计数据
                    count[x] = 0;
                }
            }
        }
    }

    private static int findMax(int[] nums) {
        int max = nums[0];
        for (int num : nums) {
            if (max <= num) {
                max = num;
            }
        }
        return max;
    }
}

 基数排序 时间复杂度: O(d*(n+10*n/10)) =  O(d*(2n)) = O(dn) ,其中 d 为最大值的位个数。

/* 10*n/10 中 n/10 为内循环_2 对每个位数的元素计数所求的平均值 */


 8. 堆排序

原理:

  1. 将待排序序列后遭成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。

  2. 将其与末尾元素进行交换,此时末尾就为最大值。

  3. 然后将剩余 n - 1 个元素重新构造成一个堆,这样就会得到 n 个元素的次小值。

  4. 如此反复执行,便能得到一个最终的大顶堆所代表的有序序列了。

Java代码:

import java.util.Arrays;

public class Sort08 {
    // 堆排序
    public static void main(String[] args) {
        int[] nums = new int[]{11, 2, 43, 16, 30, 18, 22, 8};
        sort(nums);
        System.out.println(Arrays.toString(nums));
    }

    private static void sort(int[] nums) {
        // 定义开始调整的位置
        int startIndex = (nums.length - 1) / 2;
        // 外循环开始调用 toMaxheap将初始数组转变成大顶堆
        for (int i = startIndex; i >= 0; i--) {
            toMaxheap(nums, nums.length, i);
        }
        // 完成后进行大顶堆和最后元素的调换
        for (int i = nums.length - 1; i > 0; i--) {
            swapValue(nums, i, 0);
            toMaxheap(nums, i, 0);
        }
    }

    /**
     * @param nums  要排序的数组
     * @param size  调整的元素个数
     * @param index 从哪里开始调整
     */
    private static void toMaxheap(int[] nums, int size, int index) {
        //获取左右子节点的索引
        int leftNodeindex = index * 2 + 1;
        int rightNodeindex = index * 2 + 2;
        //查找最大节点所对应的索引
        int maxIndex = index;
        if (leftNodeindex < size && nums[leftNodeindex] > nums[maxIndex]) {
            maxIndex = leftNodeindex;
        }
        if (rightNodeindex < size && nums[rightNodeindex] > nums[maxIndex]) {
            maxIndex = rightNodeindex;
        }
        //开始调换位置
        if (maxIndex != index) {
            swapValue(nums, maxIndex, index);
            //调换完之后,可能会使下面的子树不是大顶堆,所以还需要再次调换 -- 递归
            toMaxheap(nums, size, maxIndex);
        }
    }

    public static void swapValue(int[] nums, int i, int j) {
        int temp = nums[j];
        nums[j] = nums[i];
        nums[i] = temp;
    }
}

堆排序 时间复杂度: O(nlogn


 

posted @ 2023-03-08 18:17  OP_DoSOG  阅读(28)  评论(0)    收藏  举报