数据结构和算法笔记5:堆和优先队列

今天来讲一下堆,在网上看到一个很好的文章,不过它实现堆是用Golang写的,我这里打算用C++实现一下:

Golang: Heap data structure

1. 基本概念

  • 满二叉树(二叉树每层节点都是满的):

在这里插入图片描述

  • 完全二叉树:叶子节点只出现在最后一层或倒数第二层,并且节点都是向左聚拢
  • 非完全二叉树:下面的二叉树不满足完全二叉树的节点都向左聚拢,所以是非完全二叉树

在这里插入图片描述

堆也是一颗完全二叉树。

  • 小顶堆:根节点是最小值,并且子节点大于等于父节点
  • 大顶堆:根节点是最大值,并且子节点小于等于父节点
    在这里插入图片描述

由于树的特性,堆可以用数组索引的形式表示,以小顶堆为例,在下面的小顶堆里,依次从上到下从左往右给节点编号,根节点的编号是0,:

在这里插入图片描述

对应的数组为:

在这里插入图片描述
对比数组和堆,堆的索引有以下的性质:

  1. 根节点索引是0
  2. 若当前节点索引为i,如果它有父节点,父节点的索引是(i-1)/2(C++向下取整)
  3. 若当前节点索引为i,如果它有左节点,左节点的索引是2*i+1,如果它有右节点,右节点的索引是2*i+2
  4. 设数组的长度为len,最后一个非叶子节点的索引是(len-2)/2,比如上面的K是9,最后一个非叶子节点的索引是(9-2)/2=3
    在这里插入图片描述

2. 堆的基本操作

C++有heapn内置函数来实现,具体看c++重拾 STL之heap(堆)。这里我们讲解原理,下面以小顶堆为例描述堆的相关操作

2.0 交换节点操作

我们先定义交换节点的操作,为后面调整为堆做准备:

void HeapSwap(vector<int> &minHeap, int curIndex, int swapIndex)
{
    int t = minHeap[curIndex];
    minHeap[curIndex] = minHeap[swapIndex];
    minHeap[swapIndex] = t;
}

2.1 下浮操作

下浮操作是通过下浮的方式把一个完全二叉树调整为堆,具体的步骤是将它与它的左儿子,右儿子比较大小,如果不满足小顶堆的性质(当前节点的值大于等于左右孩子的节点的值),当前节点需要与左右孩子的最小值节点交换位置(否则不满足堆的性质),递归的完成这个过程。(时间复杂度是log(n))

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们定义一个swapIndex,记录需要交换调整的节点索引,如果需要调整,这个索引是当前节点和左右子节点索引的最小值,这个过程要注意判断边界条件:

void HeapSiftDown(vector<int> &minHeap, int curIndex)
{
    int leftChildIndex = 2 * curIndex + 1;  // 左孩子节点的索引
    int rightChildIndex = 2 * curIndex + 2; // 右孩子节点的索引
    int swapIndex = curIndex;               // 定义调整的节点索引

    // 判断左右孩子是否小于当前元素,如果是把swapIndex赋值为孩子索引
    if (leftChildIndex < minHeap.size() && minHeap[leftChildIndex] < minHeap[swapIndex])
        swapIndex = leftChildIndex;
    if (rightChildIndex < minHeap.size() && minHeap[rightChildIndex] < minHeap[swapIndex])
        swapIndex = rightChildIndex;

    // 判断交换索引和当前索引是不是一样,如果不一样说明要交换,然后继续SiftDown,直到到最后一个节点
    if (curIndex != swapIndex)
    {
        HeapSwap(minHeap, curIndex, swapIndex);
        HeapSiftDown(minHeap, swapIndex);
    }
}

2.2 上浮操作

上浮操作是通过上浮的方式把一个完全二叉树调整为堆,具体的步骤是将它与它的父亲节点比较大小,如果不满足小顶堆的性质(父亲的节点的值大于等于当前节点的值),当前节点与父亲节点交换位置(否则不满足堆的性质),递归的完成这个过程。(时间复杂度是log(n))

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
我们类似上浮操作定义一个swapIndex,记录需要交换调整的节点索引,如果需要调整,这个索引是父亲节点的索引,这个过程要注意判断边界条件:

void HeapSiftUp(vector<int> &minHeap, int curIndex)
{
    int parentIndex = (curIndex - 1) / 2;//父亲节点的索引
    int swapIndex = curIndex;// 定义调整的节点索引

	// 判断左右孩子是否小于当前元素,如果是把swapIndex赋值为孩子索引
    if (parentIndex >= 0 && minHeap[curIndex] < minHeap[parentIndex])
        swapIndex = parentIndex;
    // 判断交换索引和当前索引是不是一样,如果不一样说明要交换,然后继续SiftUp,直到到最后一个节点
    if (curIndex != swapIndex)
    {
        HeapSwap(minHeap, curIndex, swapIndex);
        HeapSiftUp(minHeap, swapIndex);
    }
}

2.3 给定一个数组建堆

建堆有上浮和下浮两种方法:

如果是下浮的方法,可以直接从最后一个不是叶节点的节点开始往上下浮(叶子节点没有左右孩子一定不需要交换)。这里使用了前面堆索引性质的第四条:

设数组的长度为len,最后一个非叶子节点的索引是(len-2)/2

void HeapBuild(vector<int> &array)
{
    int lastNoLeafIndex = (array.size() - 2) / 2;
    for (int i = lastNoLeafIndex; i >= 0; i--)//从最后一个不是叶节点的节点开始往上下浮
        HeapSiftDown(array, i);
}

如果是上浮的方法,则从索引为1节点开始往下上浮(根节点没有父亲节点一定不需要交换)。

void HeapBuild(vector<int> &array)
{
    for (int i = 1; i < array.size(); ++i)//从索引为1节点开始往下上浮
        HeapSiftUp(array, i);
}

使用下浮建堆的时间复杂度是O(n),而使用上浮建堆的时间复杂度是O(nlogn),建议使用下浮建堆。关于复杂度参考How can building a heap be O(n) time complexity?
在这里插入图片描述

2.4 Pop操作

pop操作是把根节点弹出返回,并重新调整剩余元素构成的数组为堆,数组的长度为len,这里我们把根节点和最后一个节点交换,中间要保留根节点的值,然后把数组调整为len-1(因为弹出一个元素了),重新用下浮调整为堆,然后返回堆的根节点的值。时间复杂度是log(n)

int HeapPop(vector<int> &minHeap)
{
    int value = minHeap[0];//保留堆的根节点的值
    int len = minHeap.size();//记录堆的大小
    HeapSwap(minHeap, 0, len - 1);//把堆的根节点和最后一个节点交换
    minHeap.resize(len - 1);//调整数组长度为len-1
    HeapSiftDown(minHeap, 0);//下浮调整为堆
    return value;//返回堆的根节点的值
}

2.5 Push操作

push操作是在数组末尾加入元素num,然后重新调整成堆。相比pop操作,push操作就简单很多了,我们先在数组末尾加入元素num,然后从最后一个元素的索引开始使用上浮即可。时间复杂度是log(n)

void HeapPush(vector<int> &minHeap, int num)
{
    minHeap.push_back(num);//在数组末尾加入元素num
    HeapSiftUp(minHeap, minHeap.size() - 1);//从最后一个元素的索引开始使用上浮
}

测试:

完整代码:

#include <iostream>
#include <vector>
using namespace std;

void HeapSiftDown(vector<int> &minHeap, int curIndex);
void HeapSiftUp(vector<int> &minHeap, int curIndex);
void HeapSwap(vector<int> &minHeap, int curIndex, int swapIndex);
void HeapBuild(vector<int> &array);
void HeapPush(vector<int> &minHeap, int num);

void HeapBuild(vector<int> &array)
{
    int lastNoLeafIndex = (array.size() - 2) / 2;
    for (int i = lastNoLeafIndex; i >= 0; i--)
        HeapSiftDown(array, i);
}

void HeapSiftDown(vector<int> &minHeap, int curIndex)
{
    int leftChildIndex = 2 * curIndex + 1; 
    int rightChildIndex = 2 * curIndex + 2;
    int swapIndex = curIndex;               

    if (leftChildIndex < minHeap.size() && minHeap[leftChildIndex] < minHeap[swapIndex])
        swapIndex = leftChildIndex;
    if (rightChildIndex < minHeap.size() && minHeap[rightChildIndex] < minHeap[swapIndex])
        swapIndex = rightChildIndex;

    if (curIndex != swapIndex)
    {
        HeapSwap(minHeap, curIndex, swapIndex);
        HeapSiftDown(minHeap, swapIndex);
    }
}

void HeapSiftUp(vector<int> &minHeap, int curIndex)
{
    int parentIndex = (curIndex - 1) / 2;
    int swapIndex = curIndex;
    if (parentIndex >= 0 && minHeap[curIndex] < minHeap[parentIndex])
        swapIndex = parentIndex;

    if (curIndex != swapIndex)
    {
        HeapSwap(minHeap, curIndex, swapIndex);
        HeapSiftUp(minHeap, swapIndex);
    }
}
void HeapSwap(vector<int> &minHeap, int curIndex, int swapIndex)
{
    int t = minHeap[curIndex];
    minHeap[curIndex] = minHeap[swapIndex];
    minHeap[swapIndex] = t;
}

int HeapPop(vector<int> &minHeap)
{
    int value = minHeap[0];
    int len = minHeap.size();
    HeapSwap(minHeap, 0, len - 1);
    minHeap.resize(len - 1);
    HeapSiftDown(minHeap, 0);
    return value;
}

void HeapPush(vector<int> &minHeap, int num)
{
    minHeap.push_back(num);
    HeapSiftUp(minHeap, minHeap.size() - 1);
}

int main()
{
    vector<int> array{9, 31, 40, 22, 10, 15, 1, 25, 91};
    cout << "The origin array is " << endl;
    for (auto &t : array)
        cout << t << " ";
    cout << endl
         << "---------------------------------------------------" << endl;

    // 建堆
    HeapBuild(array);
    cout << "After build the heap, the array is " << endl;
    for (auto &t : array)
        cout << t << " ";
    cout << endl
         << "---------------------------------------------------" << endl;

    // pop元素
    int top = HeapPop(array);
    cout << "The pop value is " << top << endl;
    cout << "After pop, the array is " << endl;
    for (auto &t : array)
        cout << t << " ";
    cout << endl
         << "---------------------------------------------------" << endl;

    // push元素
    HeapPush(array, 1);
    cout << "After push, the array is " << endl;
    for (auto &t : array)
        cout << t << " ";
    cout << endl
         << "---------------------------------------------------" << endl;
}

在这里插入图片描述

可以自行印证上面满足小顶堆。大顶堆的思路和小顶堆的思路差不多。读者可以自己实现一下。

3. 堆的相关使用

3.1 堆排序

堆排序基本的思路是:

  1. 初始化:数组建堆
  2. 数组的根节点和堆的最后一个节点交换
  3. 剩余元素重新排成堆(堆的长度减1),然后继续第2步操作直到数组的长度为1

这里也放一个算法导论的截图(不过它的根节点的索引是1),思路是差不多的:

在这里插入图片描述

我们这里使用小顶堆,小顶堆的根节点是最小值,每次第2步和后面的节点做交换,所以最后排序是从大到小(最小值根节点都放到数组的后面)。

前面的建堆是对整个数组来说的,但是对于堆排序,我们需要划定要排序数组的范围,所以我们对建堆和下浮两个操作另外定义一个函数:

  • HeapSiftDown函数

注意这里的数组越界处理改为了传入的heapLength,我们只需要对0-heapLength-1范围的数组做下浮的操作

void HeapSiftDown(vector<int> &minHeap, int curIndex, int heapLength)
{
    int leftChildIndex = 2 * curIndex + 1;  // 左孩子节点的索引
    int rightChildIndex = 2 * curIndex + 2; // 右孩子节点的索引
    int swapIndex = curIndex;               // 定义和当前索引交换的索引

    // 判断左右孩子是否小于当前元素,如果是把swapIndex换给孩子索引,注意这里的数组越界处理改为了传入的heapLength 
    if (leftChildIndex < heapLength && minHeap[leftChildIndex] < minHeap[swapIndex])
        swapIndex = leftChildIndex;
    if (rightChildIndex < heapLength && minHeap[rightChildIndex] < minHeap[swapIndex])
        swapIndex = rightChildIndex;

    // 判断交换索引和当前索引是不是一样,如果不一样说明要交换,继续SiftDown,直到到最后一个节点
    if (curIndex != swapIndex)
    {
        HeapSwap(minHeap, curIndex, swapIndex);
        HeapSiftDown(minHeap, swapIndex, heapLength);
    }
}
  • HeapBuild函数

注意这里的计算最后一个非叶子节点的索引使用了传入的heapLength,相当于对0-heapLength-1范围的数组建堆

void HeapBuild(vector<int> &array, int heapLength)
{
    int lastNoLeafIndex = (heapLength - 2) / 2;//注意这里最后一个非叶子节点的索引使用的是传入的heapLength
    for (int i = lastNoLeafIndex; i >= 0; i--)
        HeapSiftDown(array, i, heapLength);
}

OK我们可以写堆排序了,传入一个数组:

void HeapSort(vector<int> &array)
{
    int heapLength = array.size();//建堆的长度
    int len = array.size();//数组的长度
    HeapBuild(array, heapLength);
    for (int i = len - 1; i >= 1; --i)//遍历到索引1就行,索引0不需要遍历,因为只有一个数了
    {
        HeapSwap(array, 0, i);//把索引0(根节点)和索引i节点交换
        heapLength--;//建堆的长度减1
        HeapBuild(array, heapLength);//再次对0~heapLength-1的数组建堆
    }
}

测试堆排序

#include <iostream>
#include <vector>
using namespace std;
void HeapBuild(vector<int> &array, int heapLength);
void HeapSort(vector<int> &array);

void HeapBuild(vector<int> &array, int heapLength)
{
    int lastNoLeafIndex = (heapLength - 2) / 2;//注意这里最后一个非叶子节点的索引使用的是传入的heapLength
    for (int i = lastNoLeafIndex; i >= 0; i--)
        HeapSiftDown(array, i, heapLength);
}
void HeapSort(vector<int> &array)
{
    int heapLength = array.size();//建堆的长度
    int len = array.size();//数组的长度
    HeapBuild(array, heapLength);
    for (int i = len - 1; i >= 1; --i)//遍历到索引1就行,索引0不需要遍历,因为只有一个数了
    {
        HeapSwap(array, 0, i);//把索引0(根节点)和索引i节点交换
        heapLength--;//建堆的长度减1
        HeapBuild(array, heapLength);//再次对0~heapLength-1的数组建堆
    }
}
int main()
{
    vector<int> array{9, 31, 40, 22, 10, 15, 1, 25, 91};
    cout << "The origin array is " << endl;
    for (auto &t : array)
        cout << t << " ";
    cout << endl
         << "---------------------------------------------------" << endl;


    // sort元素
    HeapSort(array);
    cout << "After sort, the array is " << endl;
    for (auto &t : array)
        cout << t << " ";
    return 0;
}

可以看到从大到小进行了排序,如果用大顶堆,就是从小到大排序。
在这里插入图片描述

3.2 优先队列

优先级队列虽然也叫队列,但是和普通的队列还是有差别的。普通队列出队顺序只取决于入队顺序,而优先级队列的出队顺序总是按照元素自身的优先级。可以理解为,优先级队列是一个排序后的队列。

堆和优先级队列非常相似,一个堆就可以看作一个优先级队列。往优先级队列中插入一个元素,就相当于往堆中插入一个元素;从优先级队列中取出优先级最高的元素,就相当于取出堆顶元素(大顶堆–最大值;小顶堆–最小值)。不过优先级我们还可以自己额外定义。C++有priority_queue来实现,具体可以看c++优先队列(priority_queue)用法详解。

所以优先队列有两个操作,分别是pop弹出和push加入,pop即弹出根节点,push即把新的元素加入优先队列,两种操作过后要保证剩余的元素构成的还是一个堆。直接使用前面所说的pop和push操作即可。

4. 典型例题

347. 前 K 个高频元素

在这里插入图片描述
前K个元素,先用哈希表记录元素的频率,然后可以使用小根堆,如果队列元素超过K可以弹出根节点(最小的元素),遍历完以后,队列里剩下的就是前K大的元素。

class Solution {
public:
    static bool cmp(pair<int, int>& a, pair<int, int>& b)
    {
        return a.second > b.second;
    }
    vector<int> topKFrequent(vector<int>& nums, int k) {
        vector<int> ans;
        unordered_map<int, int> mp;
        for (auto& t: nums)
            mp[t]++;
        priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(&cmp)> que(cmp);
        for (auto it = mp.begin(); it != mp.end(); ++it)
        {
            que.push(*it);
            if (que.size() > k)
                que.pop();
        }
        while (!que.empty())
        {
            ans.push_back(que.top().first);
            que.pop();
        }
        return ans;
    }
};

关于priority_queue的比较函数cmp也可以使用仿函数:

class Solution {
public:
    class cmp {
    public:
        bool operator() (const pair<int, int> &lhs, const pair<int, int> &rhs) {
            return lhs.second > rhs.second;
        }
    };
    vector<int> topKFrequent(vector<int>& nums, int k) {
        vector<int> ans;
        unordered_map<int, int> mp;
        for (auto& t: nums)
            mp[t]++;
        priority_queue<pair<int, int>, vector<pair<int, int>>, cmp> que;
        for (auto it = mp.begin(); it != mp.end(); ++it)
        {
            que.push(*it);
            if (que.size() > k)
                que.pop();
        }
        while (!que.empty())
        {
            ans.push_back(que.top().first);
            que.pop();
        }
        return ans;
    }
};

内置类型比如int的话cmp可以直接使用greater<int>(小根堆)和less<int>(大根堆),如果比较自定义的Node类型,可以在Node里重载<

#include <queue>
#include <iostream>
using namespace std;
struct Node
{
    int x, y;
    bool operator<(const Node &rhs) const
    {
        return this->x > rhs.x; // 用x比较,这里是>,是小根堆
    }
};
int main()
{
    priority_queue<Node> que;
    que.push(Node{1, 2});
    que.push(Node{2, 1});
    que.push(Node{4, 2});
    while (!que.empty())
    {
        cout << que.top().x << " " << que.top().y << endl;
        que.pop();
    }
}

在这里插入图片描述

215. 数组中的第K个最大元素

和上题类似,我们使用一个小顶堆,遍历完整个数组,最后剩下的根节点就是第K大元素了。

class Solution {
public:

    int findKthLargest(vector<int>& nums, int k) {
        priority_queue<int, vector<int>, greater<int>> que;
        for (auto& t:nums)
        {
            que.push(t);
            if (que.size() > k)
            {
                que.pop();
            }
        }
        return que.top();
    }
};

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JAVA_Set系列集合:HashSet、LinkedHashSet、TreeSet底层详解

先看看 Set 系列集合的位置&#xff1a; Set 系列集合的特点&#xff1a; 无序&#xff1a;存取顺序不一致 如存入张三、李四、王五。而遍历获取到的是李四, 张三, 王五 不重复&#xff1a;可以去除重复无索引&#xff1a;没有带索引的方法&#xff0c;所以不能使用普通for循…

Redis缓存设计与性能优化

文章目录 多级缓存架构缓存设计缓存穿透缓存失效(击穿)缓存雪崩热点缓存key重建优化缓存与数据库双写不一致 开发规范与性能优化一、键值设计1. key名设计2. value设计bigkey的危害&#xff1a;bigkey的产生&#xff1a;如何优化bigkey 二、命令使用三、客户端使用Redis对于过期…

SpringBoot系列之MybatisPlus实现分组查询

SpringBoot系列之MybatisPlus实现分组查询 我之前博主曾记写过一篇介绍SpringBoot2.0项目怎么集成MybatisPlus的教程&#xff0c;不过之前的博客只是介绍了怎么集成&#xff0c;并没有做详细的描述各种业务场景&#xff0c;本篇博客是对之前博客的补充&#xff0c;介绍在mybat…

GitHub 一周热点汇总第7期(2024/01/21-01/27)

GitHub一周热点汇总第7期 (2024/01/21-01/27) &#xff0c;梳理每周热门的GitHub项目&#xff0c;离春节越来越近了&#xff0c;不知道大家都买好回家的票没有&#xff0c;希望大家都能顺利买到票&#xff0c;一起来看看这周的项目吧。 #1 rustdesk 项目名称&#xff1a;rust…

3个精美的wordpress律师网站模板

暗红色WordPress律师事务所网站模板 演示 https://www.zhanyes.com/qiye/23.html 暗橙色WordPress律师网站模板 演示 https://www.zhanyes.com/qiye/18.html 红色WordPress律所网站模板 演示 https://www.zhanyes.com/qiye/22.html

最新国内GPT4.0使用教程,AI绘画-Midjourney绘画V6 ALPHA绘画模型,GPT语音对话使用,DALL-E3文生图+思维导图一站式解决方案

一、前言 ChatGPT3.5、GPT4.0、GPT语音对话、Midjourney绘画&#xff0c;文档对话总结DALL-E3文生图&#xff0c;相信对大家应该不感到陌生吧&#xff1f;简单来说&#xff0c;GPT-4技术比之前的GPT-3.5相对来说更加智能&#xff0c;会根据用户的要求生成多种内容甚至也可以和…

STM32实现软件IIC协议操作OLED显示屏(2)

时间记录&#xff1a;2024/1/27 一、OLED相关介绍 &#xff08;1&#xff09;显示分辨率128*64点阵 &#xff08;2&#xff09;IIC作为从机的地址0x78 &#xff08;3&#xff09;操作步骤&#xff1a;主机先发送IIC起始信号S&#xff0c;然后发送OLED的地址0x78&#xff0c;然…

Unity 光照

光照烘培 光照模式切换为 Baked 或 Mixed&#xff0c;Baked 模式完全使用光照贴图模拟光照&#xff0c;运行时修改光照颜色不生效&#xff0c;Mixed 模式也使用光照贴图&#xff0c;并且进行一些实时运算&#xff0c;运行时修改光照颜色会生效 受光照影响的物体勾选 Contribute…

【RH850U2A芯片】Reset Vector和Interrupt Vector介绍

目录 前言 正文 1. 什么是Reset Vector 1.1 S32K144芯片的Reset Vector 1.2 RH850芯片的Reset Vector 2. 什么是Interrupt Vector 2.1 S32K144芯片的Interrupt Vector 2.2 RH850芯片的Interrupt Vector 3. Reset Vector等价于Interrupt Vector吗 4. 总结 前言 最近在…

MongoDB实战

1.MongoDB介绍 1.1 什么是MongoDB MongoDB是一个文档数据库&#xff08;以JSON 为数据模型&#xff09;&#xff0c;由C语言编写&#xff0c;旨在为WEB应用提供可扩展的高性能数据存储解决方案。 文档来自于"JSON Document"&#xff0c;并非我们一般理解的 PDF&…

【RTP】webrtc 学习3: webrtc对h264的rtp解包

rtp_rtcp\source\video_rtp_depacketizer_h264.cc【RTP】webrtc 学习2: webrtc对h264的rtp打包 中分析了打包过程的代码,这样再来看解析过程的源码就容易多了:本代码主要基于m79,m98类似。解析ParseFuaNalu 第一个字节只取 FNRI第二个字节取 原始的nalu type识别第一个分片…

【机器学习笔记】1 线性回归

回归的概念 二分类问题可以用1和0来表示 线性回归&#xff08;Linear Regression&#xff09;的概念 是一种通过属性的线性组合来进行预测的线性模型&#xff0c;其目的是找到一条直线或者一个平面或者更高维的超平面&#xff0c;使得预测值与真实值之间的误差最小化&#x…

网络安全视野:2024 年的人工智能、弹性和协作

在不断发展的网络安全环境中&#xff0c;确保公司运营安全并保障客户体验是一项复杂而关键的挑战&#xff0c;特别是对于在边缘运营的大型组织而言。当我们展望未来时&#xff0c;必须承认人工智能 (AI) 对网络安全领域的深远影响。本文深入研究了2024 年的预测&#xff0c;将其…

接口自动化测试问题汇总

本篇文章分享几个接口自动化用例编写过程遇到的问题总结&#xff0c;希望能对初次探索接口自动化测试的小伙伴们解决问题上提供一小部分思路。 sql语句内容出现错误 空格&#xff1a;由于有些字段判断是变量&#xff0c;需要将sql拼接起来&#xff0c;但是在拼接字符串时没有…

OpenCV-27 Canny边缘检测

一、概念 Canny边缘检测算法是John F.Canny与1986年开发出来的一个多级边缘检测算法&#xff0c;也被很多人认为是边缘检测的最优算法。最优边缘检测的三个主要评价标准是&#xff1a; 低错频率&#xff1a;表示出尽可能多的实际边缘&#xff0c;同时尽可能的减小噪声产生的误…

【QT+QGIS跨平台编译】之十二:【libpng+Qt跨平台编译】(一套代码、一套框架,跨平台编译)

文件目录 一、libpng介绍二、文件下载三、文件分析四、pro文件五、编译实践一、libpng介绍 PNG(Portable Network Graphics,便携式网络图形),是一种采用无损压缩算法的位图格式,支持索引、灰度、RGB三种颜色方案以及Alpha通道等特性。 PNG使用从LZ77派生的无损数据压缩算…

文心一言 VS ChatGPT :谁是更好的选择?

前言 目前各种大模型、人工智能相关内容覆盖了朋友圈已经各种媒体平台&#xff0c;对于Ai目前来看只能说各有千秋。GPT的算法迭代是最先进的&#xff0c;但是它毕竟属于国外产品&#xff0c;有着网络限制、注册限制、会员费高昂等弊端&#xff0c;难以让国内用户享受。文心一言…

移动Web——平面转换-平移

1、平面转换-平移 取值 像素单位数值百分比&#xff08;参照盒子自身尺寸计算结果&#xff09;正负均可 技巧 translate()只写一个值&#xff0c;表示沿着X轴移动单独设置X或Y轴移动距离&#xff1a;translateX()或translateY() <!DOCTYPE html> <html lang"en&q…

短视频账号矩阵系统+无人直播系统源码技术开发

短视频账号矩阵系统无人直播系统源码技术开发涉及到多个领域&#xff0c;包括但不限于前端开发、后端开发、数据库设计、网络通信等。 以下是一些基本技术的步骤和注意事项&#xff1a; 1.技术需求分析设计&#xff1a;首先&#xff0c;需要明确开发短视频账号矩阵系统和无人直…

使用mergekit 合并大型语言模型

模型合并是近年来兴起的一种新技术。它允许将多个模型合并成一个模型。这样做不仅可以保持质量&#xff0c;还可以获得额外的好处。 假设我们有几个模型:一个擅长解决数学问题&#xff0c;另一个擅长编写代码。在两种模型之间切换是一个很麻烦的问题&#xff0c;但是我们可以将…