【数据结构】二叉树的顺序结构-堆

news2025/1/20 22:03:33

【数据结构】二叉树的顺序结构-堆

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

在这里插入图片描述

1.堆的概念及结构

小堆:将根结点最小的堆叫做小堆,也叫最小堆或小根堆。

大堆:将根结点最大的堆叫做大堆,也叫最大堆或大根堆。

堆的性质

  • 堆中某个结点的值总是不大于或不小于其父结点的值。
  • 堆总是一棵完全二叉树。

在这里插入图片描述

2.堆的实现

堆的向下调整算法

现在我们给出一个数组,逻辑上看作一棵完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。

在这里插入图片描述

但是,使用向下调整算法需要满足一个前提

  • 若想将其调整为小堆,那么根结点的左右子树必须都为小堆。
  • 若想将其调整为大堆,那么根结点的左右子树必须都为大堆。

在这里插入图片描述

向下调整算法的基本思想(以建小堆为例):

  1. 从根结点处开始,选出左右孩子中值较小的孩子。
  2. 让小的孩子与其父亲进行比较。
    • 若小的孩子比父亲还小,则该孩子与其父亲的位置进行交换。并将原来小的孩子的位置当成父亲继续向下进行调整,直到调整到叶子结点为止。
    • 若小的孩子比父亲大,则不需处理了,调整完成,整个树已经是小堆了。

代码如下:

//交换函数
void Swap(int* x, int* y){
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

//堆的向下调整(小堆)
void AdjustDown(int *a, int n, int parent) {
    //child记录左右孩子中值较小的孩子的下标
    int child = 2 * parent + 1;//先默认其左孩子的值较小
    while (child < n) {
        if (child + 1 < n && a[child + 1] < a[child]){//右孩子存在并且右孩子比左孩子还小
            child++;//较小的孩子改为右孩子
        }
        if (a[child] < a[parent]){//左右孩子中较小孩子的值比父结点还小
            //将父结点与较小的子结点交换
            Swap(&a[child], &a[parent]);
            //继续向下进行调整
            parent = child;
            child = 2 * parent + 1;
        } else{//已成堆
            break;
        }
    }
}

使用堆的向下调整算法,最坏的情况下(即一直需要交换结点),需要循环的次数为:h - 1次(h为树的高度)。而h = log2(N+1)(N为树的总结点数)。所以堆的向下调整算法的时间复杂度为:O(logN)

上面说到,使用堆的向下调整算法需要满足其根结点的左右子树均为大堆或是小堆才行,那么如何才能将一个任意树调整为堆
答案很简单,我们只需要从倒数第一个非叶子结点开始,从后往前,按下标,依次作为根去向下调整即可

在这里插入图片描述

代码:

for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
    AdjustDown(php->a, n, i);
}

那么建堆的时间复杂度又是多少呢?
当结点数无穷大时,完全二叉树与其层数相同的满二叉树相比较来说,它们相差的结点数可以忽略不计,所以计算时间复杂度的时候我们可以将完全二叉树看作与其层数相同的满二叉树来进行计算。

在这里插入图片描述
总结一下:

  • 堆的向下调整算法的时间复杂度:T(n) = O (log ⁡ N)
  • 建堆的时间复杂度:T(n) = O(N)

堆的向上调整算法

当我们在一个堆的末尾插入一个数据后,需要对堆进行调整,使其仍然是一个堆,这时需要用到堆的向上调整算法。

在这里插入图片描述

向上调整算法的基本思想(以建小堆为例):

  1. 将目标结点与其父结点比较。
  2. 若目标结点的值比其父结点的值小,则交换目标结点与其父结点的位置,并将原目标结点的父结点当作新的目标结点继续进行向上调整。若目标结点的值比其父结点的值大,则停止向上调整,此时该树已经是小堆了。

在这里插入图片描述

代码如下:

//交换函数
void Swap(HPDataType *x, HPDataType *y) {
    HPDataType tmp = *x;
    *x = *y;
    *y = tmp;
}

//堆的向上调整(小堆)
void AdjustUp(HPDataType *a, int child) {
    int parent = (child - 1) / 2;
    while (child > 0) {            //调整到根结点的位置截止
        if (a[child] < a[parent]) {//孩子结点的值小于父结点的值
            //将父结点与孩子结点交换
            Swap(&a[child], &a[parent]);
            //继续向上进行调整
            child = parent;
            parent = (child - 1) / 2;
        } else {//已成堆
            break;
        }
    }
}

初始化堆

首先,必须创建一个堆类型,该类型中需包含堆的基本信息:存储数据的数组、堆中元素的个数以及当前堆的最大容量。

typedef int HPDataType;
// 堆的结构 - 顺序表
typedef struct Heap {
    HPDataType *a;
    int size;
    int capacity;
} Heap;

建堆

然后我们需要一个建堆函数,对刚创建的堆进行初始化,注意在初始化期间要将传入数据建堆。

// 堆的创建 - 建堆算法
void HeapCreate(Heap *php, HPDataType *a, int n) {
    assert(php);
    php->a = (HPDataType *) realloc(php->a, sizeof(HPDataType) * n);
    if (php->a == NULL) {
        perror("realloc fail");
        exit(-1);
    }
    // 内存复制  数组复制到php->a中
    memcpy(php->a, a, sizeof(HPDataType) * n);
    php->size = php->capacity = n;
    // 建堆算法 - 从最后一个叶子节点的父亲节点开始向下调整,然后从父亲节点的前所有节点都向下调整一次
    // 最后节点的父亲的坐标是(n-1-1)/2  n-1因为n是节点个数,坐标从0开始
    for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
        AdjustDown(php->a, n, i);
    }
}

销毁堆

为了避免内存泄漏,使用完动态开辟的内存空间后都要及时释放该空间,所以,一个用于释放内存空间的函数是必不可少的。

// 堆的销毁
void HeapDestroy(Heap *php) {
    assert(php);
    free(php->a);
    php->a = NULL;
    php->size = php->capacity = 0;
}

打印堆

// 堆的打印
void HeapPrint(Heap *php) {
    assert(php);
    for (int i = 0; i < php->size; i++) {
        printf("%d ", php->a[i]);
    }
    printf("\n");
}

堆的插入

数据插入时是插入到数组的末尾,即树形结构的最后一层的最后一个结点,所以插入数据后我们需要运用堆的向上调整算法对堆进行调整,使其在插入数据后仍然保持堆的结构。

// 堆的插入
void HeapPush(Heap *php, HPDataType x) {
    assert(php);
    // 内存满了,扩容
    if (php->size == php->capacity) {
        int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
        HPDataType *tmp = (HPDataType *) realloc(php->a, sizeof(HPDataType) * newcapacity);
        if (tmp == NULL) {
            perror("realloc fail");
            exit(-1);
        }
        php->a = tmp;
        php->capacity = newcapacity;
    }
    // 插入
    php->a[php->size] = x;
    php->size++;
    // 插入完,开始向上调整建堆
    // 将数组和child的位置传过去
    AdjustUp(php->a, php->size - 1);
}

堆的删除

堆的删除,删除的是堆顶的元素,但是这个删除过程可并不是直接删除堆顶的数据,而是先将堆顶的数据与最后一个结点的位置交换,然后再删除最后一个结点,再对堆进行一次向下调整。

原因:我们若是直接删除堆顶的数据,那么原堆后面数据的父子关系就全部打乱了,需要全体重新建堆,时间复杂度为O(N) 。若是用上述方法,那么只需要对堆进行一次向下调整即可,因为此时根结点的左右子树都是小堆,我们只需要在根结点处进行一次向下调整即可,时间复杂度为O(log ⁡N)

// 堆的删除
void HeapPop(Heap *php) {
    assert(php);
    assert(php->size > 0);

    // 堆的删除,因为不能破坏堆的结构,所以将堆顶,和堆底最后一个数据交换,然后删除堆底,堆顶再向下调整,保持堆的结构
    // 1.交换堆顶和堆底
    Swap(&php->a[0], &php->a[php->size - 1]);
    // 2.删除堆底
    php->size--;
    // 3.向下调整
    AdjustDown(php->a, php->size, 0);
}

获取堆顶的数据

获取堆顶的数据,即返回数组下标为0的数据。

// 取堆顶
HPDataType HeapTop(Heap *php) {
    assert(php);
    assert(php->size > 0);

    return php->a[0];
}

获取堆的长度

获取堆的长度,即返回堆结构体中的size变量。

// 求堆的长度
size_t HeapSize(Heap *php) {
    assert(php);

    return php->size;
}

堆的判空

堆的判空,即判断堆结构体中的size变量是否为0。

// 堆的判空
bool HeapEmpty(Heap *php) {
    assert(php);

    return php->size == 0;
}

完整代码

#pragma once
#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef int HPDataType;
// 堆的结构 - 顺序表
typedef struct Heap {
    HPDataType *a;
    int size;
    int capacity;
} Heap;


// 堆的初始化
void HeapInit(Heap *php) {
    assert(php);
    php->a = NULL;
    php->size = php->capacity = 0;
}

// 堆的打印
void HeapPrint(Heap *php) {
    assert(php);
    for (int i = 0; i < php->size; i++) {
        printf("%d ", php->a[i]);
    }
    printf("\n");
}

// 堆的销毁
void HeapDestroy(Heap *php) {
    assert(php);
    free(php->a);
    php->a = NULL;
    php->size = php->capacity = 0;
}

// 向上调整 - 大堆
void AdjustUp(HPDataType *a, int child) {
    // 1.找父亲
    int parent = (child - 1) / 2;
    // 2.跟父亲比大小,如果是大堆,知道父亲大于孩子循环结束,如果一直小于孩子,一直交换,然后循环结束条件是child==0
    while (child > 0) {
        // 孩子大于父亲则交换
        if (a[child] > a[parent]) {
            Swap(&a[child], &a[parent]);
            child = parent;
            parent = (child - 1) / 2;
        } else {
            break;
        }
    }
}

// 向下调整 - 大堆
void AdjustDown(HPDataType *a, int n, int parent) {
    // 如果是大堆,先找父亲的孩子中的大的,然后跟他交换
    // 先假设左孩子是大的,如果不是,重新设置为右孩子是大的
    int child = parent * 2 + 1;
    // child的值不会越界,所以循环条件是child < n
    while (child < n) {
        // 重新设置最大的孩子,如果右孩子大,就++child。特殊情况:最后的节点,只有左孩子,没有右孩子,所以还要加条判断,左孩子+1<n说明还有一个右孩子
        if (child + 1 < n && a[child] < a[child + 1]) {
            child++;
        }
        // 1.父亲小于孩子,交换,继续向下调整
        // 2.父亲大于孩子,跳出
        if (a[parent] < a[child]) {
            Swap(&a[parent], &a[child]);
            // 交换后,重新设置parent,找下一个位置开始向下调整
            parent = child;
            child = parent * 2 + 1;
        } else {
            break;
        }
    }
}

// 堆的插入
void HeapPush(Heap *php, HPDataType x) {
    assert(php);
    // 内存满了,扩容
    if (php->size == php->capacity) {
        int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
        HPDataType *tmp = (HPDataType *) realloc(php->a, sizeof(HPDataType) * newcapacity);
        if (tmp == NULL) {
            perror("realloc fail");
            exit(-1);
        }
        php->a = tmp;
        php->capacity = newcapacity;
    }
    // 插入
    php->a[php->size] = x;
    php->size++;
    // 插入完,开始向上调整建堆
    // 将数组和child的位置传过去
    AdjustUp(php->a, php->size - 1);
}

// 堆的删除
void HeapPop(Heap *php) {
    assert(php);
    assert(php->size > 0);

    // 堆的删除,因为不能破坏堆的结构,所以将堆顶,和堆底最后一个数据交换,然后删除堆底,堆顶再向下调整,保持堆的结构
    // 1.交换堆顶和堆底
    Swap(&php->a[0], &php->a[php->size - 1]);
    // 2.删除堆底
    php->size--;
    // 3.向下调整
    AdjustDown(php->a, php->size, 0);
}

// 取堆顶
HPDataType HeapTop(Heap *php) {
    assert(php);
    assert(php->size > 0);

    return php->a[0];
}

// 求堆的长度
size_t HeapSize(Heap *php) {
    assert(php);

    return php->size;
}

// 堆的判空
bool HeapEmpty(Heap *php) {
    assert(php);

    return php->size == 0;
}

// 交换
void Swap(HPDataType *p1, HPDataType *p2) {
    HPDataType tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}

// 堆的创建 - 建堆算法
void HeapCreate(Heap *php, HPDataType *a, int n) {
    assert(php);
    php->a = (HPDataType *) realloc(php->a, sizeof(HPDataType) * n);
    if (php->a == NULL) {
        perror("realloc fail");
        exit(-1);
    }
    // 内存复制  数组复制到php->a中
    memcpy(php->a, a, sizeof(HPDataType) * n);
    php->size = php->capacity = n;
    // 建堆算法 - 从最后一个叶子节点的父亲节点开始向下调整,然后从父亲节点的前所有节点都向下调整一次
    // 最后节点的父亲的坐标是(n-1-1)/2  n-1因为n是节点个数,坐标从0开始
    for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
        AdjustDown(php->a, n, i);
    }
}

3.堆的应用

堆排序

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

  1. 建堆
    • 升序:建大堆
    • 降序:建小堆
  2. 利用堆删除思想来进行排序建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

在这里插入图片描述

代码如下:

#include <stdio.h>
// 交换
void Swap(int *p1, int *p2) {
    int tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}

// 向下调整 - 大堆
void AdjustDown(int *a, int n, int parent) {
    // 如果是大堆,先找父亲的孩子中的大的,然后跟他交换
    // 先假设左孩子是大的,如果不是,重新设置为右孩子是大的
    int child = parent * 2 + 1;
    // child的值不会越界,所以循环条件是child < n
    while (child < n) {
        // 重新设置最大的孩子,如果右孩子大,就++child。特殊情况:最后的节点,只有左孩子,没有右孩子,所以还要加条判断,左孩子+1<n说明还有一个右孩子
        if (child + 1 < n && a[child] < a[child + 1]) {
            child++;
        }
        // 1.父亲小于孩子,交换,继续向下调整
        // 2.父亲大于孩子,跳出
        if (a[parent] < a[child]) {
            Swap(&a[parent], &a[child]);
            // 交换后,重新设置parent,找下一个位置开始向下调整
            parent = child;
            child = parent * 2 + 1;
        } else {
            break;
        }
    }
}

// 对数组进行堆排序
void HeapSort(int *a, int n) {
    // 向上调整建堆 -- N*logN
    /*for (int i = 1; i < n; ++i)
    {
    AdjustUp(a, i);
    }*/

    // 向下调整建堆 - 大堆 O(N)    _
    for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
        AdjustDown(a, n, i);
    }

    int end = n - 1;
    while (end > 0) {
        // 第一个和最后一个交换,除了最后一个,剩下的进行建堆调整,把最大的调整到堆顶
        Swap(&a[0], &a[end]);
        // end为坐标,坐标比个数多一个,所以下面的end是剩余的个数
        AdjustDown(a, end, 0);
        end--;
    }
}

int main() {
    int arr[10] = {32, 43, 56, 76, 84, 12, 45, 67, 43, 37};
    HeapSort(arr, sizeof(arr) / sizeof(int));
    for (int i = 0; i < sizeof(arr) / sizeof(int); i++) {
        printf("%d ", arr[i]);
    }
}

TOPK问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

  1. 用数据集合中前K个元素来建堆
    • 前k个最大的元素,则建小堆
    • 前k个最小的元素,则建大堆
  2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

//交换函数
void Swap(int *x, int *y) {
    int tmp = *x;
    *x = *y;
    *y = tmp;
}
//堆的向下调整(小堆)
void AdjustDown(int *a, int n, int parent) {
    //child记录左右孩子中值较小的孩子的下标
    int child = 2 * parent + 1;//先默认其左孩子的值较小
    while (child < n) {
        if (child + 1 < n && a[child + 1] < a[child]) {//右孩子存在并且右孩子比左孩子还小
            child++;                                   //较小的孩子改为右孩子
        }
        if (a[child] < a[parent]) {//左右孩子中较小孩子的值比父结点还小
            //将父结点与较小的子结点交换
            Swap(&a[child], &a[parent]);
            //继续向下进行调整
            parent = child;
            child = 2 * parent + 1;
        } else {//已成堆
            break;
        }
    }
}

int *getLeastNumbers(int *arr, int arrSize, int k, int *returnSize) {
    *returnSize = k;
    if (k == 0)
        return NULL;
    //用数组的前K个数建小堆
    int i = 0;
    int *retArr = (int *) malloc(sizeof(int) * k);
    for (i = 0; i < k; i++) {
        retArr[i] = arr[i];
    }
    for (i = (k - 1 - 1) / 2; i >= 0; i--) {
        AdjustDown(retArr, k, i);
    }
    //剩下的N-k个数依次与堆顶数据比较
    for (i = k; i < arrSize; i++) {
        if (arr[i] > retArr[0]) {
            retArr[0] = arr[i];//堆顶数据替换
        }
        AdjustDown(retArr, k, 0);//进行一次向下调整
    }
    return retArr;//返回最大的k个数
}

时间复杂度:O(k + N * log k)空间复杂度:O(k)

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

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

相关文章

Direct LiDAR-Inertial Odometry

DLIO Runing 运行效果&#xff1a; <video id“video” controls""src“data/dlio_ss.mp4” height“500” preload“none”> 论文 摘要 难点&#xff1a;快速运动 or 穿越不规则地形时降低精度&#xff0c;通常过于简单的方法而过高的计算量。本方案提出…

go-zero直连与etcd服务注册中心

go-zero中直连方式 在使用grpc是最重要的就是pb文件了&#xff0c;生成的pb文件&#xff0c;通过pb文件可以生成grpc的客户端和服务端&#xff0c;那么客户端和服务端就可以直连了&#xff0c;再次基础上可以引入etcd实现服务注册。 所有的代码都需要开发者编写&#xff0c;包…

如何面对未来的迷茫和热爱?

很多人并没有明确的热爱的事&#xff0c;就是按照一般规划安安稳稳地上学工作&#xff0c;并在一个不那么爱也不怎么讨厌的工作岗位上度过大部分人生。 首先&#xff0c;我必须说&#xff0c;我并不认为这有什么不妥。或许大部分人并不热爱自己的工作&#xff0c;但他们对自己的…

全球市场争夺战:如何提升品牌在海外市场的竞争力?

随着全球化的不断发展&#xff0c;越来越多的企业将目光投向了海外市场&#xff0c;希望能够在国际舞台上获得更大的发展机会。然而&#xff0c;海外市场的竞争激烈&#xff0c;如何有效地提升品牌在海外市场的竞争力成为了一个关键的问题。本文Nox聚星将和大家从多个方面探讨&…

CAR-NK治疗的商业化之旅

自然杀伤细胞NK细胞是一种重要的免疫效应细胞&#xff0c;能识别并杀伤病毒感染细胞和肿瘤细胞&#xff0c;不过NK细胞本身难以准确识别肿瘤细胞。科学家通过基因工程修饰&#xff0c;在NK细胞表面表达能够和肿瘤特定抗原结合的嵌合抗原受体CAR。跨膜结构域将CAR结构锚定在NK细…

DNS、ICMP协议和NAT技术

文章目录 1. DNS1.1 域名简介 2. NAT技术2.1 NAPT2.2 NAT技术的缺陷2.3 NAT和代理服务器 3. ICMP协议3.1 ping命令 4. 总结 1. DNS TCP/IP中使用IP地址和端口号来确定网络上的一台主机的一个程序&#xff0c;但是IP地址不方便记忆&#xff0c;于是人们发明了一种叫主机名的东西…

视频监控汇聚平台EasyNVR安防视频平台新版本无法对接到EasyNVS平台并报错login error,该如何解决?

安防监控系统EasyNVR视频云存储平台可实现设备接入、实时直播、录像、检索与回放、视频云存储、视频分发等视频能力服务&#xff0c;可覆盖全终端平台&#xff08;pc、手机、平板等终端&#xff09;&#xff0c;在智慧工厂、智慧工地、智慧社区、智慧校园等场景中有大量落地应用…

leetcode 671. 二叉树中第二小的节点(java)

二叉树中第二小的节点 题目描述DFS 深度优先遍历代码演示 题目描述 难度 - 简单 leetcode 671. 二叉树中第二小的节点 给定一个非空特殊的二叉树&#xff0c;每个节点都是正数&#xff0c;并且每个节点的子节点数量只能为 2 或 0。如果一个节点有两个子节点的话&#xff0c;那么…

SpringMVC增删改查(CRUD)的实现

目录 前言 一、前期准备 1.pom.xml---依赖与插件的导入 2.jdbc.properties---数据库连接 3.log4j2.xml---日志文件 4.spring-mybatis 5.spring-context 6.spring-mvc 二、增删改查的实现 1.model与mapper层的生成 2.biz层 3.工具类 4.controller层 三、测试结果 总…

MySQL——select语句的简单介绍和查询时常用的参数

select语句详解 基本的select语句 select 要查询的列名 from 要查询的表 where 限制条件; 如果要查询表的所有内容&#xff0c;则把要查询的列名用—个星号*号表示(之间的案例中都已经使用过)&#xff0c;代表要查询表中所有的列。 而大多数情况&#xff0c;我们只需要查看…

Linux——(第七章)文件权限管理

目录 一、基本介绍 二、文件/目录的所有者 1.查看文件的所有者 2.修改文件所有者 三、文件/目录的所在组 1.修改文件/目录所在组 2.修改用户所在组 四、权限的基本介绍 五、rwx权限详解 1.rwx作用到文件 2.rwx作用到目录 六、修改权限 一、基本介绍 在Linux中&…

[docker]笔记-portainer的使用

1、安装完成后输入ip加端口号打开网页&#xff0c;并再相应位置输入初始密码&#xff0c;初始密码自行设置。 2、进入主页后可以看到如下图标&#xff1a; 3、选择docker环境&#xff0c;即可展示目前docker信息 可以看到目前有1个容器&#xff0c;3个卷和4个镜像&#xff0c…

【C++ • STL】一文带你走进string

文章目录 一、STL简介二、标准库中的string类三、string类的常用接口说明2.1 string类对象的常见构造2.2 string类对象的访问及遍历操作2.2.1 元素访问2.2.2 迭代器 2.3 string类对象的容量操作2.4 string类对象的修改操作2.5 string类非成员函数 四、总结 ヾ(๑╹◡╹)&#x…

西门子PLC如何与多个三菱PLC建立无线通信?

对一个大型工厂&#xff0c;由于生产线的不断改造、新老流程的不断更新&#xff0c;这些PLC系统往往是由不同的制造商提供的。那么在智慧工厂的实现中&#xff0c;常会遇到不同品牌PLC之间需要进行相互通讯的情况。由于场地和生产能效的原因&#xff0c;在后期的系统改造中&…

docker从零部署jenkins保姆级教程(上)

jenkins&#xff0c;基本是最常用的持续集成工具。在实际的工作中&#xff0c;后端研发一般没有jenkins的操作权限&#xff0c;只有一些查看权限&#xff0c;但是我们的代码是经过这个工具构建出来部署到服务器的&#xff0c;所以我觉着有必要了解一下这个工具的搭建过程以及简…

【Java 基础篇】Java 异常处理指南:解密异常处理的关键技巧

异常是 Java 编程中不可避免的一部分。无论你是刚刚入门 Java 编程&#xff0c;还是已经有一定经验&#xff0c;了解异常处理都是非常重要的。本篇博客将向你介绍 Java 中异常的基础知识&#xff0c;帮助你理解什么是异常、为什么需要异常处理以及如何在代码中处理异常。 什么…

最新SQL注入漏洞原理及与MySQL相关的知识点

点击星标&#xff0c;即时接收最新推文 本文选自《web安全攻防渗透测试实战指南&#xff08;第2版&#xff09;》 点击图片五折购书 SQL注入漏洞简介 SQL注入是指Web应用程序对用户输入数据的合法性没有判断&#xff0c;前端传入后端的参数是攻击者可控的&#xff0c;并且参数被…

Spring MVC:视图与视图解析器

Spring MVC 前言视图视图解析器附 前言 在上一章中&#xff0c;模型数据通过域对象共享的方式返回给前端控制器 DispatcherServlet 。那么&#xff0c;把结果封装成模型视图 ModelAndView 对象返回给前端控制器 DispatcherServlet 后&#xff0c;下一步是前端控制器 Dispatche…

二分搜索树节点的查找(Java 实例代码)

目录 二分搜索树节点的查找 Java 实例代码 src/runoob/binary/BinarySearchTreeSearch.java 文件代码&#xff1a; 二分搜索树节点的查找 二分搜索树没有下标, 所以针对二分搜索树的查找操作, 这里定义一个 contain 方法, 判断二分搜索树是否包含某个元素, 返回一个布尔型变…

C#,数值计算——用于积分的梯形法(Trapezoidal Rule)的计算方法与源程序

1 文本格式 using System; namespace Legalsoft.Truffer { /// <summary> /// Routine implementing the extended trapezoidal rule. /// </summary> public class Trapzd : Quadrature { /// <summary> /// Limits …