基本排序笔记

有一些公司的笔试或者手撕代码会让写排序算法,因此进行相关的归纳,便于复习。

前言

算法稳定性:如 BA1CDA2E,其中A1==A2,即相同值的关键字,在排序前后,原先在前面的A1位置保持在A2前面,那么此排序即为稳定。

冒泡排序

  • 假设数组为array = [A,B,C,D,E,F,G]
  • 每次两两比对,移动下标,从array[0]array[1]array[1]array[2] ,……,array[n-1]array[n]
  • 【两两比对时,右边数值较小,则交换位置,否则不变,继续移动指针】
    public void sort(int[] nums) {
        for (int i = 0; i < nums.length; i++) {
            for (int j = i + 1; j < nums.length; j++) {
                //升序
                if (nums[i] > nums[j]) {
                    //交换
                    int temp = nums[i];
                    nums[j] = nums[i];
                    nums[i] = temp;
                }
            }
        }
    }
排序平均时间复杂度空间复杂度是否稳定
冒泡排序O(n²)O(1)稳定

选择排序

  • 每次遍历最小的元素
  • 最初标记在最左边未排序的第一个元素(下标),往下遍历,最后的指针落在未排序最小的元素上
  • 再将其与未排序第一个元素互换位置(一般从第一个数开始遍历查找,不会出现未排序元素<已排序元素)。
        int min;    // 无序区中最小元素位置
        for(int i=0; i<n; i++) { // 有序区的末尾位置
            min=i;
            // 找出"a[i+1] ... a[n]"之间的最小元素,并赋值给min。
            for(int j=i+1; j<n; j++) { // 无序区的起始位置
                if(a[j] < a[min])
                    min=j;
            }
            // 若min!=i,则交换 a[i] 和 a[min]。
            // 交换之后,保证了a[0] ... a[i] 之间的元素是有序的。
            int tmp = a[i];
            a[i] = a[min];
            a[min] = tmp;
        }
排序平均时间复杂度空间复杂度是否稳定
选择排序O(n²)O(1)不稳定

注:基于链表的选择排序是稳定的,基于数组实现则不稳定。由于一般排序算法时默认用数组实现,因此默认其不稳定。

插入排序

  • 取出无序区中的第1个数X,从有序区的右→左开始遍历,找出它在有序区对应的位置,即有序区中第一个小于X的元素
  • 将无序区的数据插入到有序区;一般需对有序区中的相关数据进行移位
		int i, j, k;
        for (i = 1; i < n; i++) { // 无序区
            //为a[i]在前面的a[0...i-1]有序区间中找一个合适的位置
            for (j = i - 1; j >= 0; j--) // 有序区,若比有序区大则下一个
                if (a[j] < a[i])
                    break;
            //如找到了一个合适的位置
            //将比a[i]大的数据向后移
            int temp = a[i];
            for (k = i - 1; k > j; k--)
            	a[k + 1] = a[k];
                //将a[i]放到正确位置上
                a[k + 1] = temp;
            }
        }
排序平均时间复杂度空间复杂度是否稳定
插入排序O(n²)O(1)稳定

希尔排序

每次选取一个步长(即间隔),如第一次步长为4,则array[0]array[0+4]两个元素为一组进行比较,再不断缩小步长(最后为1)比较

    public void sort(int[] nums){
        int step=nums.length; // 步长
        while (step>0){
            step=step/2; // 可自定义步长
            //分组进行插入排序
            for (int i=0;i<step;i++){
                //分组内的元素,从第二个开始
                for (int j=i+step;j<nums.length;j+=step){ // 分的小组内进行排序
                    int value=nums[j]; // 小组内的右边元素(j为右边元素下标)
                    int k;
                    for (k=j-step;k>=0;k-=step){ // 小组内的左边元素(K为左边元素下标)
                      if (nums[k]>value){
                          nums[k+step]=nums[k]; // 交换,右边元素赋值
                      }else {
                          break;
                      }
                    }
                    nums[k+step]=value; // 交换,左边元素赋值
                }
            }
        }
    }
排序平均时间复杂度空间复杂度是否稳定
希尔排序O(n^(1.3-2))O(1)不稳定

归并排序【递归】

  • 拆分:将数组对半拆分组,直至单个(单个默认有序)一组
  • 归并:两两之间比较合并(从单个一组开始)
    public static void merge(int[] a, int start, int mid, int end) {
        int[] tmp = new int[end-start+1];    // tmp是汇总2个有序区的临时区域
        int i = start;          // 第1个有序区的索引
        int j = mid + 1;        // 第2个有序区的索引
        int k = 0;              // 临时区域的索引
        while(i <= mid && j <= end) { // 归并汇总两个组至tmp[]数组
            if (a[i] <= a[j])
                tmp[k++] = a[i++];
            else
                tmp[k++] = a[j++];
        }
		// 汇总完,剩余某个组遗留(组内是有序的)元素,直接添加
        while(i <= mid)
            tmp[k++] = a[i++];
        while(j <= end)
            tmp[k++] = a[j++];
        // 将排序后的元素,全部都整合到数组a中。
        for (i = 0; i < k; i++){
            a[start + i] = tmp[i];
		}
        tmp=null;
    }
    /*
     * 归并排序(从上往下)
     */
    public static void mergeSortUp2Down(int[] a, int start, int end) {
        if(a==null || start >= end)
            return ;
        int mid = (end + start)/2;
        mergeSortUp2Down(a, start, mid); // 递归排序a[start...mid]
        mergeSortUp2Down(a, mid+1, end); // 递归排序a[mid+1...end]
        // a[start...mid] 和 a[mid...end]是两个有序空间,
        // 将它们排序成一个有序空间a[start...end]
        merge(a, start, mid, end);
    }
排序平均时间复杂度空间复杂度是否稳定
归并排序O(nlogn)O(n)稳定

注:使用了一个临时数组来存储合并的元素,空间复杂度O(n)。

快速排序【分治】

  • 选择一个元素arr[i],值为X(假设为起始位置)
  • 从右往左找到比arr[i]小的元素arr[j],将该元素赋值arr[i],可以想象为arr[j]此时没有值,是个洞
  • 由于i的数值变动了,此时从i+1开始检索比X大的数字,获得后,将其拿走替换到变动的位置arr[j],即把arr[j]填洞(同时被拿走的位置相当于有了新的洞)
    public static void quickSort(int[] a, int l, int r) {
        if (l < r) {
            int i,j,x;
            i = l;
            j = r;
            x = a[i];
            while (i < j) {
                while(i < j && a[j] > x)
                    j--; // 从右向左找第一个小于x的数
                if(i < j)
                    a[i++] = a[j]; // a[i]被赋值,同时i++
                while(i < j && a[i] < x)
                    i++; // 从左向右找第一个大于x的数
                if(i < j)
                    a[j--] = a[i];
            }
            a[i] = x;
            quickSort(a, l, i-1); /* 递归调用 */
            quickSort(a, i+1, r); /* 递归调用 */
        }
    }
排序平均时间复杂度空间复杂度是否稳定
快速排序O(nlogn)O(1)不稳定

堆排序

:必须是完全二叉树;任一节点(即任意“父子”)的值必须是其子树的最大值或最小值

  • 最大值时,称为“最大堆”,也称大顶堆;
  • 最小值时,称为“最小堆”,也称小顶堆。

最大堆通常被用来进行"升序"排序,而最小堆通常被用来进行"降序"排序。

    public void sort(int arr[]) {
        int n = arr.length;
        // 构建大顶堆
        for (int i = n / 2 - 1; i >= 0; i--) {
            heapify(arr, n, i);
        }
        // 逐步将堆顶元素与末尾元素交换,然后重新构建大顶堆
        for (int i = n - 1; i > 0; i--) {
            // 将当前堆顶元素(最大值)与末尾元素交换
            int temp = arr[0];
            arr[0] = arr[i];
            arr[i] = temp;
            // 重新构建大顶堆
            heapify(arr, i, 0);
        }
    }
    // 构建大顶堆
    void heapify(int arr[], int n, int i) {
        int largest = i;  // 最大值的索引
        int left = 2 * i + 1;  // 左子节点索引
        int right = 2 * i + 2;  // 右子节点索引
        // 左子节点大于根节点
        if (left < n && arr[left] > arr[largest]) {
            largest = left;
        }
        // 右子节点大于根节点
        if (right < n && arr[right] > arr[largest]) {
            largest = right;
        }
        // 如果最大值不是根节点,则交换并递归调整子树
        if (largest != i) {
            int swap = arr[i];
            arr[i] = arr[largest];
            arr[largest] = swap;
            heapify(arr, n, largest);
        }
    }
排序平均时间复杂度空间复杂度是否稳定
堆排序O(nlogn)O(1)不稳定

计数排序

  • 遍历获取数组最大值max,创建大小为max的数组(有点浪费空间,可以用偏移量进行改进)
  • 将数组的每个值映射到下标(不适合有负数的数组,因为数组下标起始为0),在对应下标累计出现的次数
  • 输出
    public void sort(int[] nums) {
        //查找最大值
        int max = findMax(nums);
        //寻找最小值
        int min = findMin(nums);
        //偏移量
        int gap = max - min;
        //创建统计次数新数组
        int[] countNums = new int[gap + 1];
        //将nums元素出现次数存入对应下标
        for (int i = 0; i < nums.length; i++) {
            int num = nums[i];
            countNums[num - min]++;
            nums[i] = 0;
        }
        //排序
        int index = 0;
        for (int i = 0; i < countNums.length; i++) {
            while (countNums[i] > 0) {
                nums[index++] = min + i;
                countNums[i]--;
            }
        }
    }

    public int findMax(int[] nums) {
        int max = nums[0];
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] > max) {
                max = nums[i];
            }
        }
        return max;
    }

    public int findMin(int[] nums) {
        int min = nums[0];
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] < min) {
                min = nums[i];
            }
        }
        return min;
    }
排序平均时间复杂度空间复杂度是否稳定
计数排序O(n+k)O(n)不稳定

注:虽然使用偏移量进行改进,但若是[33,1,9999,5]的数组,依旧需要建立 9999 - 1 = 9998的空间,因此,计数排序不适合偏移量过大的数组。

桶排序

   public void sort(int[] nums) {
        int len = nums.length;
        int max = nums[0];
        int min = nums[0];
        //获取最大值和最小值
        for (int i = 1; i < len; i++) {
            if (nums[i] > max) {
                max = nums[i];
            }
            if (nums[i] < min) {
                min = nums[i];
            }
        }
        //计算步长
        int gap = max - min;
        //使用列表作为桶
        List<List<Integer>> buckets = new ArrayList<>();
        //初始化桶
        for (int i = 0; i < gap; i++) {
            buckets.add(new ArrayList<>());
        }
        //确定桶的存储区间
        int section = gap / len - 1;
        //数组入桶
        for (int i = 0; i < len; i++) {
            //判断元素应该入哪个桶
            int index = nums[i] / section - 1;
            if (index < 0) {
                index = 0;
            }
            //对应的桶添加元素
            buckets.get(index).add(nums[i]);
        }
        //对桶内的元素排序
        for (int i = 0; i < buckets.size(); i++) {
            //这个底层调用的是 Arrays.sort
            // 这个api不同情况下可能使用三种排序:插入排序,快速排序,归并排序,具体看参考[5]
            Collections.sort(buckets.get(i));
        }
        //将桶内的元素写入原数组
        int index = 0;
        for (List<Integer> bucket : buckets) {
            for (Integer num : bucket) {
                nums[index] = num;
                index++;
            }
        }
    }

假设输入的数组为 [9, 20, 35, 12, 6, 28, 15, 30, 3]。
1.获取最大值和最小值:

  • max = 35, min = 3

2.计算步长:

  • gap = max - min = 35 - 3 = 32

3.初始化桶:

  • 创建一个大小为32的桶列表

4.确定桶的存储区间:

  • section = gap / len - 1 = 32 / 9 - 1 = 2

5.数组入桶:

  • 对于数组中的每个元素,根据(元素 - min) / section,并向下取整数,将元素添加到对应的桶中。
    元素 9 入桶 3 、元素 20 入桶 9 、元素 35 入桶 16 、元素 12 入桶 4 、元素 6 入桶 1 、元素 28 入桶 9 、元素 15 入桶 6 、元素 30 入桶 12 、元素 3 入桶 0

6.对桶内的元素排序:

  • 遍历桶列表,对每个桶内的元素进行排序。
    桶 0 排序后:[3] 桶 1 排序后:[6] 桶 3 排序后:[9] 桶 4 排序后:[12] 桶 6 排序后:[15] 桶 9 排序后:[20, 28] 桶 12 排序后:[30] 桶 16 排序后:[35]

7.将桶内的元素写入原数组:

  • 遍历桶列表,将每个桶内的元素按照顺序写入原数组。

最终的数组排序结果为 [3, 6, 9, 12, 15, 20, 28, 30, 35]。

排序平均时间复杂度空间复杂度是否稳定
桶排序O(n+k)O(n+k)取决于桶内排序所用算法的稳定性

基数排序

  • 待排序数组 X
  • 按照个位数进行排序,获得数组 X1
  • X1按照十位数进行排序,获得数组 X2
  • X2按照百位数进行排序,便可获得一个有序序列 。
	public void sort(int[] nums) {
        int len = nums.length;
        //最大值
        int max = nums[0];
        for (int i = 0; i < len; i++) {
            if (nums[i] > max) {
                max = nums[i];
            }
        }
        //当前排序位置
        int location = 1;
        //用列表实现桶
        List<List<Integer>> buckets = new ArrayList<>();
        //初始化size为10的一个桶
        for (int i = 0; i < 10; i++) {
            buckets.add(new ArrayList<>());
        }
        while (true) {
            //元素最高位数
            int d = (int) Math.pow(10, (location - 1));
            //判断是否排完
            if (max < d) {
                break;
            }
            //数据入桶
            for (int i = 0; i < len; i++) {
                //计算余数 放入相应的桶
                int number = ((nums[i] / d) % 10);
                buckets.get(number).add(nums[i]);
            }
            //写回数组
            int nn = 0;
            for (int i = 0; i < 10; i++) {
                int size = buckets.get(i).size();
                for (int ii = 0; ii < size; ii++) {
                    nums[nn++] = buckets.get(i).get(ii);
                }
                buckets.get(i).clear();
            }
            location++;
        }
    }
排序平均时间复杂度空间复杂度是否稳定
基数排序O(n+k)O(n+k)稳定
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值