一文搞懂优先队列及相关算法

news2024/11/26 9:37:47

大家好,我是 方圆。优先队列在 Java 中的定义是 PriorityQueue,它基于 二叉堆 数据结构实现,其中的元素并不是全部有序,但它能够支持高效地 获取或删除最值元素

二叉堆是一种特定条件的 完全二叉树,树的根节点为堆顶,最右端叶子节点为堆底,分为 小顶堆大顶堆。小顶堆堆顶元素最小,且任意节点小于等于其子节点,大顶堆堆顶元素最大,且任意节点大于等于其子节点,如下图所示:

堆.jpg

完全二叉树只有叶子节点未被填满,且叶子节点从左向右进行填充。

优先队列可以用于解决 TOP-K 问题中位数问题,下面我们先来看一些练习。如果大家想要找刷题路线的话,可以参考 Github: LeetCode

TOP-K 问题

TOP-K 问题是求最大/最小的 K 个值,解题思路很简单:以求最大的 K 个值为例,我们需要建立一个最多容纳 K 个值的小顶堆,并不断将元素入堆,直到优先队列中的元素数量为 K 时,后续再有元素入堆则需要判断它与堆顶元素的大小关系,如果它比堆顶元素大,那么堆顶元素出堆,再将这个新元素插入堆中,如此反复直到所有元素遍历完毕,最后堆顶元素即为第 K 大元素。

这个解题思路可以归纳为模板:用小顶堆解决第 K 大问题,用大顶堆解决第 K 小问题,解决第 K 大问题代码如下,供大家参考:

    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();

        for (int num : nums) {
            if (priorityQueue.size() < k) {
                priorityQueue.offer(num);
            } else {
                if (num > priorityQueue.peek()) {
                    priorityQueue.poll();
                    priorityQueue.offer(num);
                }
            }
        }

        return priorityQueue.peek();
    }

当然排序也能够解决 TOP-K 问题,但是如果数组元素过多,比如在 10 亿个数中取其中第 10 大元素,我们真的想把这 10 亿个数排序吗?这 10 亿个数能一下装进内存吗?但如果我们使用了优先队列,仅需要维护一个存储 10 个元素的优先队列即可

小顶堆
  • 215. 数组中的第K个最大元素

本题是典型的应用小顶堆解决第K大元素的问题,大家直接按照解题模板解题即可。

  • 703. 数据流中的第 K 大元素

大家解完上一题再拿这题练练手。

  • 692. 前K个高频单词

本题相对来说没有那么容易,但是本质是一样的,我们来一起看一下:首先我们需要定义数据结构并借助 map 来统计词频,之后使用小顶堆来保存 K 大元素,注意结果需要按照词频从大到小排列,所以我们需要将出堆元素倒序排列,可以借助链表并采用头插法来实现,题解如下:

    static class WordNum implements Comparable<WordNum> {

        String word;

        int num;

        public WordNum(String word) {
            this.word = word;
            this.num = 1;
        }

        @Override
        public int compareTo(WordNum o) {
            if (this.num != o.num) {
                return this.num - o.num;
            } else {
                return o.word.compareTo(this.word);
            }
        }
    }

    public List<String> topKFrequent(String[] words, int k) {
        HashMap<String, WordNum> map = new HashMap<>();
        for (String word : words) {
            if (map.containsKey(word)) {
                map.get(word).num++;
            } else {
                map.put(word, new WordNum(word));
            }
        }

        PriorityQueue<WordNum> priorityQueue = new PriorityQueue<>();
        for (WordNum value : map.values()) {
            if (priorityQueue.size() < k) {
                priorityQueue.offer(value);
            } else {
                if (value.compareTo(priorityQueue.peek()) > 0) {
                    priorityQueue.poll();
                    priorityQueue.offer(value);
                }
            }
        }

        LinkedList<String> res = new LinkedList<>();
        while (!priorityQueue.isEmpty()) {
            res.addFirst(priorityQueue.poll().word);
        }

        return res;
    }
大顶堆
  • LCR 159. 库存管理 III面试题 17.14. 最小K个数

这两题都是应用大顶堆求解前K小元素的标准题目,很简单,参考题解如下:

    public int[] inventoryManagement(int[] stock, int cnt) {
        int[] res = new int[cnt];
        if (cnt == 0) {
            return res;
        }
        
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>((x, y) -> y - x);
        for (int s : stock) {
            if (priorityQueue.size() < cnt) {
                priorityQueue.offer(s);
            } else {
                if (s < priorityQueue.peek()) {
                    priorityQueue.poll();
                    priorityQueue.offer(s);
                }
            }
        }

        for (int i = 0; i < res.length; i++) {
            res[i] = priorityQueue.poll();
        }
        
        return res;
    }
  • 1405. 最长快乐字符串

本题不是 TOP-K 问题,而是使用大顶堆中能获取到其中最大元素的特性来构造字符串。根据题意,需要保证不能有三个相同的字符相同且字符串最长,那么我们可以每次获取到其中数量最大的字符进行拼接,每连续拼接两次则需要获取下一个数量最大的字符再拼接,这样能够保证我们拼接的字符串最长,题解如下:

    static class CharNum implements Comparable<CharNum> {

        char c;

        int num;

        public CharNum(char c, int num) {
            this.c = c;
            this.num = num;
        }

        @Override
        public int compareTo(CharNum o) {
            return o.num - this.num;
        }
    }

    public String longestDiverseString(int a, int b, int c) {
        PriorityQueue<CharNum> charNums = new PriorityQueue<>();
        if (a != 0) {
            charNums.offer(new CharNum('a', a));
        }
        if (b != 0) {
            charNums.offer(new CharNum('b', b));
        }
        if (c != 0) {
            charNums.offer(new CharNum('c', c));
        }

        StringBuilder res = new StringBuilder();
        while (!charNums.isEmpty()) {
            CharNum one = charNums.poll();
            int length = res.length();
            
            if (length >= 2 && res.charAt(length - 1) == one.c && res.charAt(length - 2) == one.c) {
                CharNum two = charNums.poll();
                if (two != null) {
                    res.append(two.c);
                    if (--two.num > 0) {
                        charNums.offer(two);
                    }
                    charNums.offer(one);
                } else {
                    break;
                }
            } else {
                res.append(one.c);
                if (--one.num > 0) {
                    charNums.offer(one);
                }
            }
        }

        return res.toString();
    }

中位数问题

求数据流的中位数问题可以借助两个堆来解决:大顶堆保存数据流中前一半元素,小顶堆保存数据流中后一半元素,如果数据流中元素数量为奇数,那么取大顶堆的堆顶元素为中位数;如果数据流中的元素数为偶数,分别取大顶堆和小顶堆的堆顶元素,求和除以二即为中位数。

  • 295. 数据流的中位数

大家可以拿这道题来练练手,还是比较简单的,题解如下:

class MedianFinder {

    PriorityQueue<Integer> left;

    PriorityQueue<Integer> right;

    public MedianFinder() {
        left = new PriorityQueue<>((x, y) -> y - x);
        right = new PriorityQueue<>();
    }

    public void addNum(int num) {
        if (left.size() == right.size()) {
            right.offer(num);
            left.offer(right.poll());
        } else {
            left.offer(num);
            right.offer(left.poll());
        }
    }

    public double findMedian() {
        return left.size() == right.size() ? (left.peek() + right.peek()) / 2.0 : left.peek();
    }
}

到这里我们基本了解了堆的特性和用法,接下来以小顶堆为例,看下它的实现,我们要弄清楚它是如何保证堆顶元素是最小元素的。

堆的数组实现

使用数组可以高效地实现小顶堆,我们规定索引 0 处的元素不使用,那么堆中所有元素是从索引 1 开始记录的,相应地,若当前节点的索引为 k,那么它的父节点为 k/2(根节点除外),左子节点索引为 2k,右子节点索引为 2k + 1。当堆中每个节点都小于等于它的两个子节点时,我们称它为 堆有序

由上至下的堆有序化(swim)

如果堆的有序状态因为某个节点变得比它的父节点更小而被打破,那么我们需要通过交换它和它的父节点来修复有序关系,交换完成后,这个节点比它的两个子节点都小,但这个节点仍然可能比它的父节点更小,所以我们需要一遍一遍地(while)修复有序关系,直到遇到更小的父节点或者到达根节点位置。这个方法实现起来比较简单,如下:

    /**
     * 由下至上堆有序化(上浮)
     */
    private void swim(int index) {
        while (index > 1 && nums[index] < nums[index / 2]) {
            swap(index, index / 2);
            index /= 2;
        }
    }
由下至上的堆有序化(sink)

如果堆的有序状态因为某个节点变得比它的两个子节点之一更大而被打破,那么我们需要通过交换它和它的两个子节点中较小的节点来修复有序关系,同样地,我们也需要一遍一遍地(while)来执行这个逻辑,直到它的两个子节点都比它大或者它到达了堆底。该方法的实现如下:

    /**
     * 由上至下堆有序化(下沉)
     */
    private void sink(int index) {
        while (index * 2 <= size) {
            int son = index * 2;
            if (son + 1 <= size && nums[son + 1] < nums[son]) {
                son++;
            }
            if (nums[index] > nums[son]) {
                swap(index, son);
                index = son;
            } else {
                break;
            }
        }
    }

swim() 和 sink() 方法是实现堆的基础 API,有了这两个方法我们能够轻松地实现插入元素和最小元素出堆的操作

  • 插入元素:增加堆的大小并将新元素直接插入到数组末尾,再让这个新元素上浮到合适的位置即可

  • 最小元素出堆:将堆顶元素移除并将堆底元素放到堆顶,减小堆的大小并让这个元素下沉到合适的位置即可

按照以上逻辑,固定大小堆的代码实现如下:

public class MyPriorityQueue {

    int[] nums;

    int size;

    int capacity;

    public MyPriorityQueue(int n) {
        // 不使用 0 索引
        nums = new int[n + 1];
        this.capacity = n;
        this.size = 0;
    }

    public int size() {
        return size;
    }

    /**
     * 获取堆顶元素,即最小值
     */
    public int peek() {
        if (size == 0) {
            return -1;
        }

        return nums[1];
    }

    /**
     * 元素入堆操作,先将该元素赋值在堆底,并不断地和比自己大的父节点交换位置
     */
    public void offer(int num) {
        if (size + 1 <= capacity) {
            nums[++size] = num;
            swim(size);
        }
    }

    /**
     * 由下至上堆有序化(上浮)
     */
    private void swim(int index) {
        while (index > 1 && nums[index] < nums[index / 2]) {
            swap(index, index / 2);
            index /= 2;
        }
    }

    /**
     * 元素出堆操作,将堆顶元素与堆底元素交换位置,并不断地和比自己小的子节点交换位置
     */
    public int poll() {
        if (size == 0) {
            return -1;
        }

        int res = nums[1];
        swap(1, size--);
        sink(1);

        return res;
    }

    /**
     * 由上至下堆有序化(下沉)
     */
    private void sink(int index) {
        while (index * 2 <= size) {
            int son = index * 2;
            if (son + 1 <= size && nums[son + 1] < nums[son]) {
                son++;
            }
            if (nums[index] > nums[son]) {
                swap(index, son);
                index = son;
            } else {
                break;
            }
        }
    }

    private void swap(int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}
多路归并

堆能用来解决多路归并问题,它可以将 多个有序的输入流归并成一个有序的输出流,下面是使用堆来解决多路归并问题的样例:

public class Multiway {

    public static void main(String[] args) {
        Multiway multiway = new Multiway();
        multiway.merge(new int[][]{{1, 3, 9}, {2, 4, 8}, {5, 6, 7}});
    }

    public void merge(int[][] streams) {
        int N = streams.length;
        // streamIndex, numIndex, num
        PriorityQueue<int[]> priorityQueue = new PriorityQueue<>(Comparator.comparingInt(x -> x[2]));
        for (int i = 0; i < streams.length; i++) {
            priorityQueue.offer(new int[]{i, 0, streams[i][0]});
        }

        while (!priorityQueue.isEmpty()) {
            int[] min = priorityQueue.poll();
            System.out.println(min[2]);

            if (min[1] + 1 < streams[min[0]].length) {
                priorityQueue.offer(new int[]{min[0], min[1] + 1, streams[min[0]][min[1] + 1]});
            }
        }
    }
}

我来简单解释下多路归并的逻辑:将多个有序输入流中最小元素放入小根堆中,在多路归并过程中每个输入流至多与堆关联一个元素,之后执行出堆操作,每次操作将堆顶元素移除,这保证了每次移除的元素都是当前输入流中最小的元素,每有元素出堆,都要检查该输入流中是否还有元素,有的话则继续插入堆中,没有则证明该输入流已经被遍历完了,再继续处理其他输入流直到堆中没有任何元素即可。

相关练习
  • 373. 查找和最小的 K 对数字

多路归并是将多个有序的输入流归并成一个有序的输入流,根据题意可知,输入流是两个输入数组组成的数值对,那么数值对输入流该如何构造呢?想一想,我们可以让 nums1 中的每个元素去结合 nums2 中的每个元素,那么 nums1 中有几个元素就能组成几个输入流,现在我们创建小根堆按照数值对的和排序,并将每个输入流中的元素加入堆中,按照上述多路归并样例中的逻辑处理即可,题解如下:

    public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        // nums1Index, nums2Index
        PriorityQueue<int[]> priorityQueue = new PriorityQueue<>(Comparator.comparingInt(x -> (nums1[x[0]] + nums2[x[1]])));
        for (int i = 0; i < nums1.length; i++) {
            priorityQueue.offer(new int[]{i, 0});
        }

        List<List<Integer>> res = new ArrayList<>();
        while (res.size() < k && !priorityQueue.isEmpty()) {
            int[] element = priorityQueue.poll();
            res.add(Arrays.asList(nums1[element[0]], nums2[element[1]]));

            if (element[1] + 1 < nums2.length) {
                priorityQueue.offer(new int[]{element[0], element[1] + 1});
            }
        }

        return res;
    }
堆排序

我们可以通过大根堆对数组进行排序,重复调用最大元素出堆的操作,并将该元素放在数组的末端,这样的排序我们称它为 堆排序。堆排序可以分成两个阶段,建堆下沉排序

  • 建堆:将原始数组构建成大根堆。在前文我们说过,堆是一棵完全二叉树,记树的节点数量为 n,完全二叉树的叶子节点数量为 n/2 或 n/2 + 1,那么非叶子节点数量为 ⌊n⌋,而每个叶子节点我们都可以看成只包含一个元素的堆,所以这些节点是需要进行建堆操作的,我们只需要对非叶子节点建堆即可

  • 下沉排序:建堆完成后,堆顶为最大元素,我们每次将最大元素排在数组最右端未被占用的位置即可,这个过程和选择排序类似,但是所需的比较次数少得多,因为堆能高效地从未排序部分找到最大元素

堆排序的代码如下:

public class DeapSort {

    public void sort(int[] nums) {
        int n = nums.length - 1;

        // 1. 建堆
        for (int i = n / 2; i > 0; i--) {
            sink(nums, i, n);
        }
        // 2. 下沉排序
        while (n > 1) {
            swap(nums, n, 1);
            sink(nums, 1, --n);
        }
    }

    private void sink(int[] nums, int i, int n) {
        while (i * 2 <= n) {
            int max = i * 2;
            if (max + 1 <= n && nums[max + 1] > nums[max]) {
                max++;
            }
            if (nums[i] < nums[max]) {
                swap(nums, i, max);
                i = max;
            } else {
                break;
            }
        }
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

算法特性:

  • 时间复杂度:O(nlogn)

  • 空间复杂度:O(1)

  • 原地排序

  • 非稳定排序

  • 非自适应排序

贪心算法

在贪心算法中使用堆是借助堆能获取到当前元素中的最值的特点,我们需要确定 堆中需要放什么元素和它的排序规则,以及 元素进堆和出堆的条件

  • 1705. 吃苹果的最大数目

我们把需要确定的条件列一下:

  • 堆中保存的元素和排序规则:保存的元素是苹果的数量和它的过期时间,并按照过期时间从早到晚排序,优先将过期早的苹果吃掉保证吃掉的苹果数目最多

  • 元素进堆的条件:到了哪天便把哪天成熟的苹果进堆

  • 元素出堆条件:苹果过期或者把苹果吃光了,元素出堆

逻辑不难,题解如下:

    public int eatenApples(int[] apples, int[] days) {
        // applesNum, endDay
        PriorityQueue<int[]> priorityQueue = new PriorityQueue<>(Comparator.comparingInt(x -> x[1]));

        int day = 0;
        int res = 0;
        while (day < apples.length || !priorityQueue.isEmpty()) {
            if (day < apples.length && apples[day] > 0) {
                priorityQueue.offer(new int[]{apples[day], day + days[day]});
            }

            // 过期的都扔掉
            while (!priorityQueue.isEmpty() && day >= priorityQueue.peek()[1]) {
                priorityQueue.poll();
            }
            if (!priorityQueue.isEmpty()) {
                int[] apple = priorityQueue.peek();
                apple[0]--;
                res++;
                if (apple[0] == 0) {
                    priorityQueue.poll();
                }
            }
            day++;
        }

        return res;
    }
  • 1834. 单线程 CPU

本题相对来说困难一些,我们一起来看一下:

  • 堆中保存的元素和排序规则:想要获取最佳的处理任务顺序,那么 CPU 一定优先处理时间短的任务,如果处理时间相同,则优先处理索引小的任务,堆需要按照这个规则对任务进行排序,那么我们其中保存的元素就包括了索引和任务的处理时间信息

  • 元素进堆的条件:当前时间大于任务开始执行的时间

  • 元素出堆条件:任务根据排序规则逐个出堆即可,代表 CPU 逐个执行任务

任务的开始时间影响 CPU 是否能执行该任务,那么我们需要对所有的任务按照开始时间递增的顺序进行排序,从而能获取到第一个能够执行的任务,后续如果优先队列中没有任务可处理时,那么直接让当前时间等于下一个需要处理的任务的开始时间,题解如下:

    static class IndexProcessTime implements Comparable<IndexProcessTime> {

        int index;

        int beginTime;

        int processTime;

        public IndexProcessTime(int index, int beginTime, int processTime) {
            this.index = index;
            this.beginTime = beginTime;
            this.processTime = processTime;
        }

        @Override
        public int compareTo(IndexProcessTime o) {
            return this.beginTime - o.beginTime;
        }
    }

    public int[] getOrder(int[][] tasks) {
        ArrayList<IndexProcessTime> list = new ArrayList<>(tasks.length);
        for (int i = 0; i < tasks.length; i++) {
            list.add(new IndexProcessTime(i, tasks[i][0], tasks[i][1]));
        }
        Collections.sort(list);
        // taskIndex, processTime
        PriorityQueue<IndexProcessTime> priorityQueue = new PriorityQueue<>((x, y) -> {
            if (x.processTime == y.processTime) {
                return x.index - y.index;
            } else {
                return x.processTime - y.processTime;
            }
        });

        int index = 0;
        int[] res = new int[list.size()];
        int beginTime = 0;
        for (int i = 0; i < list.size() || !priorityQueue.isEmpty(); ) {
            while (i < list.size() && beginTime >= list.get(i).beginTime) {
                IndexProcessTime task = list.get(i++);
                priorityQueue.offer(new IndexProcessTime(task.index, task.beginTime, task.processTime));
            }

            if (!priorityQueue.isEmpty()) {
                IndexProcessTime task = priorityQueue.poll();
                res[index++] = task.index;
                // 增加处理时间
                beginTime += task.processTime;
            } else {
                beginTime = list.get(i).beginTime;
            }
        }

        return res;
    }
  • 871. 最低加油次数

本题虽然是困难的题目,但是我觉得是这三道题里最简单的了,本题的解题思路可以用一句话来概括:记录车能跑的最远距离,并把经过的加油站按照油量加入大顶堆,每次油不够的时候就拿油最多的加上,这样能保证加油的次数最少,最终如果油加完了还不够则返回 -1,否则为最少加油次数,不过我们还是要把需要关注的点说明下:

  • 堆中保存的元素和排序规则:保存的元素是加油站油量,油量最多的在堆顶

  • 元素进堆的条件:车行驶的距离超过加油站的距离,则加油站油量进堆

  • 元素出堆条件:车行驶距离不够到达目的地,油量最多的出堆,给车加油

    public int minRefuelStops(int target, int startFuel, int[][] stations) {
        Arrays.sort(stations, Comparator.comparingInt(x -> x[0]));
        // 保存油量
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>((x, y) -> y - x);

        int index = 0;
        int res = 0;
        while (startFuel < target) {
            while (index < stations.length && startFuel >= stations[index][0]) {
                priorityQueue.offer(stations[index++][1]);
            }
            if (priorityQueue.isEmpty()) {
                return -1;
            } else {
                startFuel += priorityQueue.poll();
                res++;
            }
        }

        return res;
    }

巨人的肩膀

  • 《Hello 算法》:第八章 堆

  • 《算法 第四版》:第 2.4 章 优先队列

  • 《算法导论 第三版》:第六章 堆排序

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1175863.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vivo 网络端口安全建设技术实践

作者&#xff1a;vivo 互联网安全团队 - Peng Qiankun 随着互联网业务的快速发展&#xff0c;网络攻击的频率和威胁性也在不断增加&#xff0c;端口是应用通信中的门户&#xff0c;它是数据进出应用的必经之路&#xff0c;因此端口安全也逐渐成为了企业内网的重要防线之一&…

公司团建小游戏开发小程序游戏互动小游戏

在现代工作环境中&#xff0c;团队合作和员工士气是取得成功的关键因素。为了增强团队合作、提升员工士气&#xff0c;并促进员工之间的互动&#xff0c;公司团建小游戏成为了一种备受欢迎的方式。本文将探讨如何开发公司团建小游戏&#xff0c;以达到这些目标。 1. 游戏概念 …

2023下半年软考信息系统项目管理师上午真题及答案

1.( B )不属于项目建议书的核心内容。 A.项目的必要性 B.初步可行性研究 C.项目的市场预测 D.项目建设必需的条件 解析&#xff1a; 2.在监控项目工作过程中&#xff0c;当遇到变更请求时&#xff0c;为使项目工作绩效重新与项目管理计划致&#xff0c;而进行的有目的的活动…

【双指针+简化去重操作】Leetcode 15 三数之和

【双指针简化操作】Leetcode 15 三数之和 解法1 解法1 新建一个嵌套列表&#xff1a;List<List<Integer>> result new List<>(); 初始化一个ArrayList并直接赋值&#xff1a;ArrayList<Integer> result new ArrayList<>(Arrays.asList(1, 2…

又来安利了,这个Itbuilder在线数据库设计工具用起来太顺手了

对于测试、开发、DBA、运维来说&#xff0c;数据库是再熟悉不过了。 我们都知道如今的数据是多么复杂和难以管理&#xff0c;但幸运的是有数据库设计工具可以帮助我们&#xff0c;可以在市场上找到很多的数据库设计工具&#xff0c;包括itbuilder。这些数据库设计工具可以帮助我…

如何选择SVM中最佳的【核函数】

参数“kernel"在sklearn中可选以下几种 选项&#xff1a; 接下来我们 就通过一个例子&#xff0c;来探索一下不同数据集上核函数的表现。我们现在有一系列线性或非线性可分的数据&#xff0c;我们希望通过绘制SVC在不同核函数下的决策边界并计算SVC在不同核函数下分类准确…

如何对ppt文件设置修改权限?

PPT文件会应用在会议、演讲、课件等工作生活中&#xff0c;当我们制作好了PPT之后&#xff0c;保护内容防止在演示时出错是很重要的&#xff0c;那么如何将PPT文件设置成禁止修改模式呢&#xff1f;今天分享几个方法给大家。 方法一 将PPT文件直接保存或者另存为一份文件&…

【Effective Modern C++】条款2:理解auto类型推导

条款2&#xff1a;理解auto类型推导 条款1中&#xff0c;模板类型推导的函数模板形如&#xff1a; template<typename T> void f(ParamType param);当变量采用auto声明时&#xff0c;auto扮演了模板中的T这个角色&#xff0c;而变量的类型扮演的是ParamType的角色。 条…

如何提高CRM系统的使用率?

​ CRM客户管理系统采购以后不投入使用&#xff0c;或者用了却用不好&#xff0c;都是极大的浪费。在知道CRM系统使用率低的原因之后&#xff0c;就要通过有效的方法提升CRM使用率。下面整理了六个方法&#xff0c;告诉您如何提高CRM系统的使用率。 有人演奏不出优美的曲子&a…

本地如何安装多个node版本

使用nvm 1官网下载 Releases coreybutler/nvm-windows GitHub 2.安装完 nvm -v检查是否按照成功 三、nvm的使用 安装完毕后&#xff0c;找到安装的路径&#xff0c;一些简单配置&#xff0c;打开setting.txt (是下载完毕之后自动帮你生成npm 下载node包之后不用重复安装…

【23真题】满分!最高150分!评级A+!

今天分享的是23年西南交通大学924的信号与系统试题及解析。这套卷子是回忆版&#xff0c;我已经尽力去还原了&#xff0c;全网仅此一份。如果有疏漏的地方&#xff0c;欢迎大家和我反馈。 本套试卷难度分析&#xff1a;平均分在124分&#xff0c;最高分有满分150分&#xff01…

高清Logo素材无忧:这5个网站解决所有问题!

今天给大家分享几个素材网站&#xff0c;基本上可以下载各大企业的 Logo&#xff0c;而且还是矢量格式哦~ 即时设计 即时设计是一款国产免费的 Logo 在线设计制作工具&#xff0c;浏览器内打开即用&#xff0c;对于使用系统没有任何限制。在即时设计&#xff0c;你可以从 0 到…

【数智化案例展】正官庄——全球商业数智化实践案例

‍ Marketingforce案例 本项目案例由Marketingforce投递并参与数据猿与上海大数据联盟联合推出的《2023中国数智化转型升级创新服务企业》榜单/奖项”评选。 大数据产业创新服务媒体 ——聚焦数据 改变商业 电商行业持续发展&#xff0c;而对于品牌来说&#xff0c;面对多个分…

陈海波:OpenHarmony技术领先,产学研深度协同,生态蓬勃发展

11月4日&#xff0c;以“技术筑生态&#xff0c;智联赢未来”为主题的第二届OpenHarmony技术大会在北京隆重举办。本次大会由OpenAtom OpenHarmony&#xff08;简称“OpenHarmony"&#xff09;项目群技术指导委员会&#xff08;TSC&#xff09;主办&#xff0c;由华为技术…

第十一章 Python 常用标准库

系列文章目录 第一章 Python 基础知识 第二章 python 字符串处理 第三章 python 数据类型 第四章 python 运算符与流程控制 第五章 python 文件操作 第六章 python 函数 第七章 python 常用内建函数 第八章 python 类(面向对象编程) 第九章 python 异常处理 第十章 python 自定…

解决Lightroom Classic无法使用修改照片的问题

Lightroom Classic是一款广泛使用的照片编辑软件&#xff0c;但在使用过程中&#xff0c;可能会遇到无法修改照片的问题。修改照片这个模块无法使用&#xff0c;功能按钮呈现灰色&#xff0c;无法点击。本文将介绍几种常见的解决方法&#xff0c;帮助您快速解决Lightroom无法使…

Banana Pi BPI-W3 RK3588平台驱动调试篇 [ PCIE篇一 ] - PCIE的开发指南

RK3588平台驱动调试篇 [ PCIE篇 ] - PCIE的开发指南 1、PCIE接口概述 PCIe&#xff08;Peripheral Component Interconnect Express&#xff09;是一种用于连接计算机内部组件的高速接口标准。以下是关于PCIe接口的简要介绍&#xff1a; 高速传输&#xff1a; PCIe接口提供了…

springboot 文件上传 阿里云OSS

一、介绍 文件上传&#xff0c;是指将本地图片、视频、音频等文件上传到服务器上&#xff0c;可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛&#xff0c;我们经常发抖音、发朋友圈都用到了文件上传功能。 实现文件上传服务&#xff0c;需要有存储的支持&…

01-Spring中事务的实现和事务的属性

银行账户转账异常 需求: 实现act-001账户向act-002账户转账10000,要求两个账户的余额一个减成功一个加成功,即执行的两条update语句必须同时成功或失败 实现步骤 第一步: 引入项目所需要的依赖 <?xml version"1.0" encoding"UTF-8"?> <proj…

LeetCode | 206. 反转链表

LeetCode | 206. 反转链表 OJ链接 这里有两个思路 我们先来看第一个思路&#xff1a; 创建一个新的链表&#xff0c;然后将原链表头插头插需要保存下一个的地址&#xff0c;再头插 代码如下&#xff1a; struct ListNode* reverseList(struct ListNode* head) {struct ListN…