【数据结构初阶】——第八节.优先级队列(小根堆的模拟实现)

news2024/11/29 10:42:59

 作者简介:大家好,我是未央;

博客首页:未央.303

系列专栏:Java初阶数据结构

每日一句:人的一生,可以有所作为的时机只有一次,那就是现在!!!

目录

文章目录

前言

引言

一、堆的概念

二、堆的性质 

三、堆的操作

3.1 向下调整算法

3.2 小根堆的创建

3.3 向上调整算法

3.4 堆的删除(堆顶元素的删除)

四、优先级队列的模拟实现(小根堆)

总结


今天我们将进入到有关堆的有关内容的学习,以及有关优先级队列的相关使用,要对堆的概念,性质,操作有很熟悉的认识和了解,接下来就让我们进入到今天的学习当中吧!!!!!!


引言

我们之前学过队列,那么什么是优先级队列呢?

举个例子

队列是一种先进先出(FIFO)的数据结构但是有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,在这种情况下使用队列就不行了,比如玩游戏的时候突然女朋友一通电话,游戏屏幕瞬间被电话占领,这时候就应该优先处理电话。

在这种情况下,我们的数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新对象,这种数据结构就是优先级队列(PriorityQueue)。

但其实这种对优先级队列的定义是不严谨的,严谨的说法是:

优先级队列是逻辑结构是小根堆存储结构是动态数组(到达上限,容量自动加一)的集合类。
所以说优先级队列不是数据结构中的概念,而是java中的集合类。
但在JDK1.8中的PriorityQueue底层使用了堆的数据结构,而堆实际就是在完全二叉树的基础之上进行了一些元素的调整,所以说优先级队列是一种数据结构也未尝不可。

 既然优先级队列的底层用到了堆这种数据结构,那么什么是堆呢?

一、堆的概念

前提知识:二叉树的顺序存储

使用数组存储二叉树的方式,就是将二叉树按照层序遍历放入数组;

一般只适合完全二叉树,因为非完全二叉树会有空间的浪费;

在这里插入图片描述

 而堆其实就是一棵完全二叉树,所以就可以用数组来储存堆这一数据结构;

总结:堆就是一颗顺序存储的完全二叉树,底层是一个数组;

  1. 堆逻辑上是一颗完全二叉树

  2. 堆物理上是保存在数组中

二、堆的性质 

既然是完全二叉树,那么之前我们得出来的那些完全二叉树的性质也同样适用于堆;

下面是一个完全二叉树,也同样是一个堆

也就是说已知父亲结点的下标就能求出孩子结点的下标,同理如果知道了孩子结点的下标也能求出他所对应的父亲结点的下标;

堆的分类:大根堆和小根堆;

  1. 小根堆:每一个父结点的值均小于等于其对应的子结点的值,而根结点的值就是最小的。
  2. 大根堆:每一个父结点的值均大于等于其对应的子结点的值,而根结点的值就是最大的。

图示说明:

如此一来我们就又得到了堆的另外两条性质:

  1. 堆中某个节点的值总是不大于或不小于其父节点的值。
  2. 堆总是一棵完全二叉树。
  3. 但完全二叉树不一定能称作堆(可能无序,不满足堆的性质)

三、堆的操作

3.1 向下调整算法

图示说明:

 此时我们看到,这个二叉树整体上不符合堆的性质,但是其根部的左子树和右子树均满足堆的性质。 

注意上面说的也是向下调整的前提即——必须得确保根结点的左右子树均为小堆才可;

接下来,就要进行向下调整,确保其最终是个堆。只需三步。

  1. 找出左右孩子中最小的那个
  2. 跟父亲比较,如果比父亲小,就交换
  3. 更新父亲结点(继续向下调整,因为经过上一次调整后下面的子树可能就不满足小根堆的性质了)

 调整过程如图所示:

你可能问会问:你是怎样得到父亲结点和他所对应的孩子结点的位置的,不就是根据完全二叉树的性质呀!

左孩子结点的下标 = (2 * 父亲结点的下标 + 1)

右孩子结点的下标 = (2 * 父亲结点的下标 + 2)

父亲结点的下标 = (孩子结点的下标 - 1) / 2

 但要注意我们的数组长度是有限的,表示孩子结点的下标不能超过数组所允许的最大下标

所以说,只要我们一开始给出要调整的二叉树的根节点坐标(父亲结点坐标),我们就能将给定的这棵完全二叉树变成小根堆;

代码示例:

/**
     * 向下调整——使得当前子树为小根堆
     * @param root 是每棵子树的根结点的下标
     * 向下调整的时间复杂度O(log2n)(最坏情况下就是树的高度)
     */
    public void shiftDown(int root) {
        int parent = root; // 父亲结点的坐标
        int child = 2 * parent + 1; // 获取左孩子结点的坐标
        // 为什么不能child下标要小于usedSize,因为当前数组的最大下标就是usedSize - 1,如果大于或等于usedSize就越界了
        while (child < usedSize) { // 每个子树在调整的时候,是按从上到下,当child的下标小于usedSize时候就结束
            // 这一步目的是找出孩子结点最大的那个值,然后在让该值和父亲结点比较(不过先要确定孩子结点存在)
            if (child + 1 < usedSize && elem[child] > elem[child + 1]) {
                child = child + 1;
            }
            if (elem[child] < elem[parent]) {
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                parent = child; // 从上向下调整子树,更新父亲结点的下标
                child = 2 * parent + 1; // 更新左孩子孩子结点的下标
            }
            // 因为我们是从上向下调整子树,当我们在调整上面的子树时,下面的子树一定是调整好了的,如果上面都已经满足小根堆,下面也一定满足
            else {
                break; // 此时已经是小根堆了,不需要再次调整,直接退出循环接着调整下一个子树
 
            }
        }
    }

代码解析:


3.2 小根堆的创建

说明:我们通常所用到的堆,要么是小根堆、要么就是大根堆,这里我们所创建的就一个小根堆;

既然说堆是一种可以用数组来储存,那么如果给了一个无序的数组,怎样把他变成一个有序的小根堆(完全二叉树)呢? 

我们是不是就应该在创建的过程中,及时的调整数组中的元素位置,让该堆成为一个小根堆(根结点的值大于左右孩子结点的值)

举例说明:

 

 那该怎么调整呢?

好像每一个子树都不符合小根堆的定义啊!那么我们就需要从从倒数的第一个非叶子节点的子树开始调整一直调整到根节点的树,这样从上到下调整不同的子树后就可以调整成堆;

至于每个子树的调节方式,那不就是我们刚才提到的从根结点开始对指定的子树进行向下调节

 为什么要从下面的较小的子树调节到上面的大子树?

这是因为我们刚才的向下调整法,所能作用的二叉树是:整体上不符合堆的性质,但是其根部的左子树和右子树均满足堆的性质

当我们从上向下进行调节时,即从倒数的第一个非叶子节点的子树开始调节时,此时的二叉树一定是满足这个性质的,当下面的子树调节好了后,你会发现此时原来不满足这个性质的上面的那些子树竟然也满足这些性质了(因为他下面的子树已经调节过了)

思路如下图所示(这里是虽然构建的是大堆根,但思路是一样的): 

 代码如下:

public class MyHeap {
    int[] elem;   // 我们说过堆可以用数组来存放
    int usedSize; // 优先级队列中有效元素的个数
    MyHeap() {
        elem = new int[10]; // 构造方法初始化一下,堆(数组)的容量
    }
    public void createHeap(int[] arrays) {
        for (int i = 0; i < arrays.length; i++) {
            elem[i] =arrays[i]; // 元素初始化,给堆中元素赋值
            ++usedSize;
        }
        // 倒数的第一个非叶子节点的子树开始调节,一直调节到根结点(根节点在数组中的下标为0)
        // 注意这里是usdSize - 1 - 1,因为父亲结点的下标 = (孩子结点的下标 - 1) / 2,
        // 我们是从倒数的第一个非叶子节点的子树开始调节的,而该子树的孩子结点坐标为usedSize - 1
        for (int i = (usedSize - 1 - 1) / 2; i >= 0 ; --i) {
            shiftDown(i); // 从下面的子树一直调到上面的子树
        }
    }
 
    /**
     * 向下调整——使得当前子树为小根堆
     * @param root 是每棵子树的根结点的下标
     * 向下调整的时间复杂度O(log2n)(最坏情况下就是树的高度)
     */
    public void shiftDown(int root) {
        int parent = root; // 父亲结点的坐标
        int child = 2 * parent + 1; // 获取左孩子结点的坐标
        // 为什么不能child下标要小于usedSize,因为当前数组的最大下标就是usedSize - 1,如果大于或等于usedSize就越界了
        while (child < usedSize) { // 每个子树在调整的时候,是按从上到下,当child的下标小于usedSize时候就结束
            // 这一步目的是找出孩子结点最大的那个值,然后在让该值和父亲结点比较(不过先要确定孩子结点存在)
            if (child + 1 < usedSize && elem[child] > elem[child + 1]) {
                child = child + 1;
            }
            if (elem[child] < elem[parent]) {
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                parent = child; // 从上向下调整子树,更新父亲结点的下标
                child = 2 * parent + 1; // 更新左孩子孩子结点的下标
            }
            // 因为我们是从上向下调整子树,当我们在调整上面的子树时,下面的子树一定是调整好了的,如果上面都已经满足小根堆,下面也一定满足
            else {
                break; // 此时已经是小根堆了,不需要再次调整,直接退出循环接着调整下一个子树
 
            }
        }
    }
}

3.3 向上调整算法

我们由堆的插入引入该算法

堆的插入不像先前顺序表一般,可以头插,任意位置插入等等,因为是堆,要符合大根堆或小根堆的性质,不能改变堆原本的结构,所以尾插才是最适合的,并且尾插后还要检查是否符合堆的性质。

图示说明:

 为了确保在插入数字10后依然是个小根堆,所以要将10和28交换,依次比较父结点parent和子结点child的大小,当父小于子结点的时候,就返回,反之就一直交换,直到根部。

由前文的得知的规律,parent = (child - 1) / 2,我们可以从下到上调整子树,不断的更新child和parent下标,直到根部15

🔔需要注意的是:我们操控的是数组,但要把它想象成二叉树

画图演示调整过程:

代码示例:

/**
     * 从下向上调整子树
     * @param child 要调整子树的孩子坐标
     */
    public void shiftUp(int child) {
        int parent = (child - 1) / 2; // 通过孩子坐标得出父亲坐标
        while (child > 0) { // 当孩子坐标等于0时,说明根结点已经调整完毕,退出循环
            // 因为之前的该子树的孩子结点的值肯定满足小根堆,所以我们只用考虑新加入的孩子结点的值
            if (elem[parent] > elem[child]) {
                int tmp = elem[parent];
                elem[parent] = elem[child];
                elem[child] = tmp;
                // 更新
                child = parent;
                parent = (child - 1) / 2;
            }
            else {
                break; // 因为我们是从下向上调整子树的,我们上面的结点之前是满足小根堆的,所以如果下面也满足了,说明整个子树都满足了,直接退出循环
            }
        }
    }

3.4 堆的插入

讲完了说完了向上调整算法,那堆的插入不就很简单了吗!

代码示例:

/**
     * 入队,当要保证入队后仍是小根堆
     * @param val 要入队的元素
     */
    public void offerHeap(int val) {
        if (isFull()) {
            elem = new int[2 * usedSize];
        }
        else {
            elem[usedSize] = val; // 把新添加的元素放到数组中最后的位置
            ++usedSize;
            shiftUp(usedSize - 1); // 放完后就从新元素的位置——从下向上调整该子树
        }
    }
 
    /**
     * 从下向上调整子树
     * @param child 要调整子树的孩子坐标
     */
    public void shiftUp(int child) {
        int parent = (child - 1) / 2; // 通过孩子坐标得出父亲坐标
        while (child > 0) { // 当孩子坐标等于0时,说明根结点已经调整完毕,退出循环
            // 因为之前的该子树的孩子结点的值肯定满足小根堆,所以我们只用考虑新加入的孩子结点的值
            if (elem[parent] > elem[child]) {
                int tmp = elem[parent];
                elem[parent] = elem[child];
                elem[child] = tmp;
                // 更新
                child = parent;
                parent = (child - 1) / 2;
            }
            else {
                break; // 因为我们是从下向上调整子树的,我们上面的结点之前是满足小根堆的,所以如果下面也满足了,说明整个子树都满足了,直接退出循环
            }
        }
    }
    
     // 判断当前队列是否已满
    public boolean isFull() {
        return usedSize == elem.length;
    }

3.4 堆的删除(堆顶元素的删除)

堆的删除和堆的插入很相似,就是把要删除的根节点和堆的最后一个元素互换位置,然后从上向下调整子树:

删除步骤:

  1. 将堆顶元素对堆中最后一个元素交换
  2. 将堆中有效数据个数减少一个
  3.  对堆顶元素进行向下调整

举例说明:

  删除过程如下:

代码示例:

/**
     * 出队删除,每次删除的都是优先级高的元素——当前完全二叉树的根结点
     * 出队后仍要保证该二叉树是小根堆
     */
    public void pollHeap() {
        if (isEmpty()) {
            System.out.println("当前优先级队列为空!");
            return;
        }
        // 将当前的队首元素与队尾元素互换位置,然后将向下调整以队首元素为根节点的那个子树,使得满小根堆
        int tmp = elem[usedSize - 1];
        elem[usedSize - 1] = elem[0];
        elem[0] = tmp;
        --usedSize; // 既然是删除,有效元素个数要减一
        shiftDown(0);
    }
 
    /**
     * 向下调整——使得当前子树为小根堆
     * @param root 是每棵子树的根结点的下标
     * 向下调整的时间复杂度O(log2n)(最坏情况下就是树的高度)
     */
    public void shiftDown(int root) {
        int parent = root; // 父亲结点的坐标
        int child = 2 * parent + 1; // 获取左孩子结点的坐标
        // 为什么不能child下标要小于usedSize,因为当前数组的最大下标就是usedSize - 1,如果大于或等于usedSize就越界了
        while (child < usedSize) { // 每个子树在调整的时候,是按从上到下,当child的下标小于usedSize时候就结束
            // 这一步目的是找出孩子结点最大的那个值,然后在让该值和父亲结点比较(不过先要确定孩子结点存在)
            if (child + 1 < usedSize && elem[child] > elem[child + 1]) {
                child = child + 1;
            }
            if (elem[child] < elem[parent]) {
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                parent = child; // 从上向下调整子树,更新父亲结点的下标
                child = 2 * parent + 1; // 更新左孩子孩子结点的下标
            }
            // 因为我们是从上向下调整子树,当我们在调整上面的子树时,下面的子树一定是调整好了的,如果上面都已经满足小根堆,下面也一定满足
            else {
                break; // 此时已经是小根堆了,不需要再次调整,直接退出循环接着调整下一个子树
 
            }
        }
    }
 
    // 判断当前优先级队列是否为空
    public boolean isEmpty() {
        return usedSize == 0;
    }

四、优先级队列的模拟实现(小根堆)

到了这里,相信大家对小根堆的各自操作应该已经不陌生了吧!那么接下来就让我们用这些操作模拟实现一个优先级队列:

代码示例:

/**
 * 用小根堆模拟实现优先级队列
 */
public class MyHeap {
    int[] elem;
    int usedSize; // 优先级队列中有效元素的个数
    MyHeap() {
        elem = new int[10]; // 构造方法初始化一下
    }
    public void createHeap(int[] arrays) {
        for (int i = 0; i < arrays.length; i++) {
            elem[i] =arrays[i]; // 元素初始化
            ++usedSize;
        }
        for (int i = (usedSize - 1) / 2; i >= 0 ; --i) {
            shiftDown(i); // 从下面的子树一直调到上面的子树
        }
    }
 
    /**
     * 向下调整——使得当前子树为小根堆
     * @param root 是每棵子树的根结点的下标
     * 向下调整的时间复杂度O(log2n)(最坏情况下就是树的高度)
     */
    public void shiftDown(int root) {
        int parent = root; // 父亲结点的坐标
        int child = 2 * parent + 1; // 获取左孩子结点的坐标
        // 为什么不能child下标要小于usedSize,因为当前数组的最大下标就是usedSize - 1,如果大于或等于usedSize就越界了
        while (child < usedSize) { // 每个子树在调整的时候,是按从上到下,当child的下标小于usedSize时候就结束
            // 这一步目的是找出孩子结点最大的那个值,然后在让该值和父亲结点比较(不过先要确定孩子结点存在)
            if (child + 1 < usedSize && elem[child] > elem[child + 1]) {
                child = child + 1;
            }
            if (elem[child] < elem[parent]) {
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                parent = child; // 从上向下调整子树,更新父亲结点的下标
                child = 2 * parent + 1; // 更新左孩子孩子结点的下标
            }
            // 因为我们是从上向下调整子树,当我们在调整上面的子树时,下面的子树一定是调整好了的,如果上面都已经满足小根堆,下面也一定满足
            else {
                break; // 此时已经是小根堆了,不需要再次调整,直接退出循环接着调整下一个子树
 
            }
        }
    }
 
    /**
     * 入队,当要保证入队后仍是小根堆
     * @param val 要入队的元素
     */
    public void offerHeap(int val) {
        if (isFull()) {
            elem = new int[2 * usedSize];
        }
        else {
            elem[usedSize] = val; // 把新添加的元素放到数组中最后的位置
            ++usedSize;
            shiftUp(usedSize - 1); // 放完后就从新元素的位置——从下向上调整该子树
        }
    }
 
    /**
     * 从下向上调整子树
     * @param child 要调整子树的孩子坐标
     */
    public void shiftUp(int child) {
        int parent = (child - 1) / 2; // 通过孩子坐标得出父亲坐标
        while (child > 0) { // 当孩子坐标等于0时,说明根结点已经调整完毕,退出循环
            // 因为之前的该子树的孩子结点的值肯定满足小根堆,所以我们只用考虑新加入的孩子结点的值
            if (elem[parent] > elem[child]) {
                int tmp = elem[parent];
                elem[parent] = elem[child];
                elem[child] = tmp;
                // 更新
                child = parent;
                parent = (child - 1) / 2;
            }
            else {
                break; // 因为我们是从下向上调整子树的,我们上面的结点之前是满足小根堆的,所以如果下面也满足了,说明整个子树都满足了,直接退出循环
            }
        }
    }
    // 判断当前队列是否已满
    public boolean isFull() {
        return usedSize == elem.length;
    }
 
    /**
     * 出队删除,每次删除的都是优先级高的元素——当前完全二叉树的根结点
     * 出队后仍要保证该二叉树是小根堆
     */
    public void pollHeap() {
        if (isEmpty()) {
            System.out.println("当前优先级队列为空!");
            return;
        }
        // 将当前的队首元素与队尾元素互换位置,然后将向下调整以队首元素为根节点的那个子树,使得满小根堆
        int tmp = elem[usedSize - 1];
        elem[usedSize - 1] = elem[0];
        elem[0] = tmp;
        --usedSize; // 既然是删除,有效元素个数要减一
        shiftDown(0);
    }
    // 判断当前优先级队列是否为空
    public boolean isEmpty() {
        return usedSize == 0;
    }
 
    /**
     * 获取堆顶元素
     * @return
     */
    public int peekHeap() {
        if (isEmpty()) {
            System.out.println("当前优先级队列为空!");
            return -1;
        }
        return elem[0];
    }
}
 

总结

今天的内容就介绍到这里,我们下一节内容再见!!!!

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

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

相关文章

API接口三问

一、API数据接口可以给我们带来哪些便利 API数据接口可以给我们带来以下便利&#xff1a; 数据共享&#xff1a;API允许数据在不同的应用程序之间共享。这使得数据转移更容易&#xff0c;因为不需要手动复制和粘贴数据内容。 程序集成&#xff1a;API作为中间件&#xff0c;可…

20年+资深审稿人:什么情况下建议文章大小修、拒稿或接收?

文章进入外审后&#xff0c;作者最终可能会得到大小修、接收或拒稿的意见。那么&#xff0c;审稿人是怎么给出这些不同意见的呢&#xff1f;有哪些方面需要作者提前了解呢&#xff1f; Surgery 发布过一篇文章&#xff0c;里面调查了一些具有20年审稿经验、平均年龄69岁的编委会…

ThreadLocal八股文

目录 1. 为什么要⽤ ThreadLocal? 2. ThreadLocal 的原理是什么&#xff1f; 3. 为什么⽤ ThreadLocal 做 key&#xff1f; 4. Entry 的 key 为什么设计成弱引⽤&#xff1f; 5. ThreadLocal 真的会导致内存泄露&#xff1f; 6. 如何解决内存泄露问题&#xff1f; 7. T…

1M分辨率 中国各城市绿地数据的获取

城市绿地系统是城市总体规划的有机组成部分&#xff0c;反映了城市的自然属性。在人类选址建造城市之初&#xff0c;大多将城市选择在和山、川、江、湖相毗邻的地方&#xff0c;它给予城市的形态、功能布局及城市景观以很大影响。先有自然&#xff0c;后有城市&#xff0c;自然…

时间序列预测(三)基于Prophet+XGBoost的销售额预测

时间序列预测&#xff08;三&#xff09;基于ProphetXGBoost的销售额预测 前面我们介绍了如何使用Prophet和LSTM&#xff0c;不知道你们发现了没有&#xff0c;前者似乎太简单了&#xff0c;后者呢好像又很复杂。那有没有什么很好的方法能很好的中和下呢&#xff1f; 已知的有…

入门黑客(网络安全)需要准备什么?

之所以写这篇文章呢&#xff0c;是觉得大时代的发展&#xff0c;我们这个专业越来越受到重视了&#xff0c;所以&#xff0c;也想以自己的一些拙见&#xff0c;能帮到想入门网络安全的朋友 1.关于网上的培训 如果你想快速获得知识&#xff0c;培训无疑是最快最有效的捷径&…

ANR概述

系列文章目录 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 例如&#xff1a;第一章 Python 机器学习入门之pandas的使用 文章目录 系列文章目录前言一、ANR是什么&#xff1f;二、ANR超时阈值三、前后与台区别1.前台与后台服务的区…

核心案例 | 南京理工大学空地协同编队控制系统建设项目

项目名称&#xff1a;空地协同编队控制系统建设项目 场 地&#xff1a;室内/室外 关 键 词&#xff1a;自主导航与SLAM、集群协同决策、集群控制 南京理工大学核心案例(1) 01 项目背景 本项目通过集群四旋翼无人机、天地协同集群控制开发环境、无人机协同集群控制系统…

音频转换成mp3的方法

把音频转换为MP3格式是因为MP3格式可以更好地压缩音频文件&#xff0c;减小文件大小&#xff0c;便于存储和传输。此外&#xff0c;MP3格式已成为流行的音频格式之一&#xff0c;许多设备和软件都支持MP3格式&#xff0c;使得MP3格式成为了一种通用的音频格式。总的来说&#x…

log4Qt史上最详细介绍、编译和使用

文章目录 Log4Qt介绍下载log4qt源码测试例子&#xff08;源码使用&#xff09;将log4qt源码添加到工程测试代码日志配置文件&#xff1a;测试结果 总结log4qt更多请参考&#xff1a; 使用Log4Qt动态库一、创建Qt工程&#xff0c;命名为libLog4Qt二、在项目根目录下新建文件夹3r…

Simulink使能(Enable)、触发(Triggered)模块及其子系统的应用

Simulink中的使能&#xff08;Enable&#xff09;和触发&#xff08;Triggered&#xff09;模块及其子系统可以用于控制模型中的仿真运行时间和采样周期&#xff0c;从而提高模型的仿真效率和精度。 使用使能子系统 创建一个在控制信号为正值时执行的子系统。使用触发子系统 创…

如何系列 如何在Windows和Linux安装Nginx

文章目录 Windows一 下载Nginx二 启动Nginx三 验证 Linux一 安装依赖项二 下载Nginx源码包三 安装四 验证五 常用命令附录 Nginx是一款高性能的开源Web服务器和反向代理服务器&#xff0c;被广泛用于构建现代化的Web应用和提供静态内容。本篇博文将教你如何在Windows和Linux操作…

Vue 前端代码多地部署(打包后配置动态IP)

Vue 前端代码多地部署&#xff08;打包后配置动态IP&#xff09; 需求一、使用 config.json二、使用 config.js 需求 vue 代码打包之后&#xff0c;需要在多个地方部署。正常操作是&#xff1a;先改 ip 地址&#xff0c;再打包。这样每换一个地方部署&#xff0c;就需要重新打…

将自己写的nginx.conf运行到阿里云linux服务器上

首先 你要保证自己的nginx.conf没有问题 可以先在本地运行一下 然后来到nginx.conf文件的所在目录 利用 scp -r ./nginx.conf 用户名(如果之前没设置过就是 root)服务器公网地址:/etc/nginx/将文件传到服务器上去 这里需要注意 如果你的服务器之前没有装过nginx 是没有这个目…

C语言实现随机点名器

目录 1、程序描述 2、程序功能 3、功能详细实现过程 学生结构体声明和定义 菜单&#xff08;menu&#xff09;函数 文件读取和保存函数 查询函数 点名函数 rand函数 点名函数实现 点名次数归零函数 字体颜色变化函数 4、运行效果 5、源码分享 1、程序描述 只使用…

ANR实战案例 2 - 不同线程状态ANR示例

系列文章目录 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 例如&#xff1a;第一章 Python 机器学习入门之pandas的使用 文章目录 系列文章目录前言一、Blocked状态示例1.启动初始化阻塞案例trace1.tx 2.ConcurrentHashMap分段锁优…

互联网营销之何谓真需求-想知道如何挖掘真需求看这篇就对了

互联网营销思维是以爆品为核心的迭代思维&#xff0c;本文结合“生日蛋糕”、“方便面”、“蜜雪冰城”几个小例子&#xff0c;以及我们具体的工作&#xff0c;展开聊聊什么是“真需求”。 1. 互联网营销和传统营销的区别 1.1 传统的营销思维&#xff1a; “定位4P&#xff0…

亚马逊云科技:使用Inf2实例运行大语言模型GPT-J-6B基础设施

在2019年的亚马逊云科技re:Invent上&#xff0c;亚马逊云科技发布了Inferentia芯片和Inf1实例这两个基础设施。Inferentia是一种高性能机器学习推理芯片&#xff0c;由亚马逊云科技定制设计&#xff0c;其目的是提供具有成本效益的大规模低延迟预测。时隔四年&#xff0c;2023年…

金融行业软件测试面试必备:答案详解与干货技巧

大家好&#xff0c;今天我要和大家分享的是我多年从事金融行业软件测试的心得体会。由于金融行业涉及到的数据量非常大&#xff0c;系统功能也十分复杂&#xff0c;因此在招聘软件测试人员时&#xff0c;往往会提出一些具有挑战性的问题。 作为一个资深面试官&#xff0c;我也…

Android aidl及binder基础知识巩固

作者&#xff1a;义华 1、什么是binder binder是android framework提供的&#xff0c;用于跨进程方法调用的机制&#xff0c;具有安全高效等特点。 我们知道&#xff0c;在 Android 系统中&#xff0c;每个应用程序都运行在一个独立的进程中&#xff0c;各个进程之间需要进行…