每日算法总结——java TreeSet、数组旋转、堆排序扩展、桶排序思想下的排序

一、Java TreeSet

TreeSet是通过TreeMap实现的一个有序的、不可重复的集合,底层维护的是红黑树结构。
当TreeSet的泛型对象不是java的基本类型的包装类时,对象需要重写Comparable#compareTo()方法
具体参考知乎

二、数组旋转

最近在写LeetCode的时候,遇到了很多数组旋转的问题,因此决定在这里做个总结,可能不全,后续再补充。
【问题】:给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

2.1 多次反转

先反转全部数组,在反转前k个,最后在反转剩余的,如下:
来源leetcode

public void rotate(int[] nums, int k) {
    int length = nums.length;
    k %= length;
    reverse(nums, 0, length - 1);//先反转全部的元素
    reverse(nums, 0, k - 1);//在反转前k个元素
    reverse(nums, k, length - 1);//接着反转剩余的
}

//把数组中从[start,end]之间的元素两两交换,也就是反转
public void reverse(int[] nums, int start, int end) {
    while (start < end) {
        int temp = nums[start];
        nums[start++] = nums[end];
        nums[end--] = temp;
    }
}

当然也可以再调整下,先反转前面的,接着反转后面的k个,最后在反转全部,原理都一样

2.2 环形旋转

类似约瑟夫环一样,把数组看作是环形的,每一个都往后移动k位



但这里有一个坑,如果nums.length%k=0,也就是数组长度为k的倍数,这个会原地打转,对于这个问题我们可以使用一个数组visited表示这个元素有没有被访问过,如果被访问过就从他的下一个开始,防止原地打转。

public static void rotate(int[] nums, int k) {
    int hold = nums[0];
    int index = 0;
    int length = nums.length;
    boolean[] visited = new boolean[length];
    for (int i = 0; i < length; i++) {
        index = (index + k) % length;
        if (visited[index]) {
            //如果访问过,再次访问的话,会出现原地打转的现象,
            //不能再访问当前元素了,我们直接从他的下一个元素开始
            index = (index + 1) % length;
            hold = nums[index];
            i--;
        } else {
            //把当前值保存在下一个位置,保存之前要把下一个位置的
            //值给记录下来
            visited[index] = true;
            int temp = nums[index];
            nums[index] = hold;
            hold = temp;
        }
    }
}

三、堆排序扩展

【补充】:优先级对队列结构,就是堆结构(对于Java,PriorityQueue底层就是堆结构

扩展题目

已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。

【解题思路】:假设k = 6,准备一个小根堆,遍历前7个数字放入小根堆,此时小根堆的最小值一定是整个数组的最小值(因为任何一个数的位置与它排完序的位置距离都不会超过k = 6),将小根堆的最小值弹出并放入数组的0位置处,把下一个数(第8个数)放入小根堆,重复…重复,直到将整个数组排好序。

复杂度分析:每个数放入小根堆的操作为 O ( l o g   k ) O(log\ k) O(log k),所以时间复杂度为 O ( N l o g   k ) O(Nlog\ k) O(Nlog k),当k很小的时候,就可认为这是一个 O ( N ) O(N) O(N)的算法。
【代码实现】:

public class SortArrayDistanceLessK {
    /**
     * 已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,
     * 并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。
     */
    public static void sortArrayDistanceLessK(int[] arr, int k) {
        // 默认小根堆
        Queue<Integer> heap = new PriorityQueue<>();
        int index = 0;
        for (; index <= k; index++) {
            heap.add(arr[index]);
        }
        int i = 0;
        for (;!heap.isEmpty() && index < arr.length; index++, i++) {
            arr[i] = heap.poll();
            heap.add(arr[index]);
        }
        while (!heap.isEmpty()) {
            arr[i++] = heap.poll();
        }
    }
}

小根堆在Java中就是优先级队列,因此可以直接使用PriorityQueue,但是需要注意几点:

  • 扩容问题PriorityQueue在底层是用数组作为堆的实际结构,但它不像我们手写的堆,提前规定了整个数组的大小,PriorityQueueadd()时若发现空间不够会进行成倍扩容,单次扩容代价是 O ( N ) O(N) O(N)(数组拷贝)
    • 若一共add() N N N个数,则会经历 l o g   N log\ N log N 次扩容。
    • 所以扩容的总代价为 O ( N ∗ l o g   N ) O(N*log\ N) O(Nlog N),每个数平均扩容代价为 O ( l o g N ) O(log N) O(logN)
  • ⭐️无法改变值PriorityQueue无法处理这样的情况:改变某一个节点的值,并对该节点执行heapify/heapInsert操作,PriorityQueue只能删除该节点后,添加一个新数。
    • 这也是为什么很多面试场合不得不手写堆的原因,我们自己写的堆是可以单独对某一个节点进行heapify/heapInsert的,需要重点分辨

四、桶排序思想下的排序

1、计数排序
算法描述
  1. 找出待排序的数组中最大和最小的元素;
  2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
适用场景

排序目标要能够映射到整数域,其最大值最小值应当容易辨别。
高中生考试的总分数,显然用0-750就OK啦(不考虑小数);又比如一群人的年龄,用个0-150应该就可以了,再不济就用0-200。另外,计数排序需要占用大量空间,它比较适用于数据比较集中的情况。
排序范围/数量非常大的时候,就要慎重考虑这种排序方法

2、基数排序

排序过程:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

算法描述
  1. 取得数组中的最大数,并取得位数;
  2. arr为原始数组,从最低位开始取每个位组成一个桶
  3. 对arr中的每个数首先根据个位从左到右依次放入各个桶中,然后按照从左到右、先进先出的顺序将所有的数从桶中拿出来
  4. 再根据十位、百位、…、最高位,重复操作3。
    动图图源知乎
    虽然算法思路是这样的但是代码可以写的更灵活一点(再次感叹左神老师是真的🐂)
public class RadixSort {
    /**
     * 基数排序算法描述
     * 1. 取得数组中的最大数,并取得位数;
     * 2. arr为原始数组,从最低位开始取每个位组成一个桶;
     * 3. 对arr中的每个数首先根据个位从左到右依次放入各个桶中,然后按照从左到右、先进先出的顺序将所有的数从桶中拿出来
     * 4. 再根据十位、百位、…、最高位,重复操作3。
     */
    public static void radixSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        radixSort(arr, 0, arr.length - 1, maxbits(arr));
    }

    /**
     * 求数组中最大数的位数
     *
     * @param arr 数组
     * @return 最大数的位数
     */
    public static int maxbits(int[] arr) {
        int max = Integer.MIN_VALUE;
        for (int i : arr) {
            max = Math.max(i, max);
        }
        int count = 0;
        while (max != 0) {
            count++;
            max /= 10;
        }
        return count;
    }

    /**
     * 对数组的指定范围进行基数排序————通用的写法
     *
     * @param arr   数组
     * @param l     左边界
     * @param r     右边界
     * @param digit 要排序的数中,最大数的位数
     */
    public static void radixSort(int[] arr, int l, int r, int digit) {
        final int radix = 10;
        int i = 0, j = 0;
        // 有多少个数就准备多少个辅助空间
        int[] bucket = new int[r - l + 1];
        // 有多少位就进出多少次
        for (int d = 1; d < digit; d++) {
            // 10个空间
            // count[0], 当前位i是0的数有多少个
            // count[1], 当前位i是1的数有多少个
            // ...
            int[] count = new int[radix];
            // 计算处在d位上各个数字对应的arr中数据量
            for (i = l; i < radix; i++) {
                j = getDigit(arr[i], d);
                count[j]++;
            }
            // 求count的前缀和
            for (i = 1; i < radix; i++) {
                count[i] = count[i] + count[i - 1];
            }
            for (i = r; i >= l; i--) {
                j = getDigit(arr[i], d);
                bucket[(count[j]--) - 1] = arr[i];
            }
            // 拷贝回arr
            for (i = l, j = 0; i <= r; i++, j++) {
                arr[i] = bucket[j];
            }
        }
    }

    /**
     * 返回num的第d位数字
     */
    public static int getDigit(int num, int d) {
        return ((num / ((int) Math.pow(10, d - 1))) % 10);
    }
}
实战:最大间距
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值