请回答数据结构-【并查集&LRUCache】


image-20220524134006623

并查集

Intro of Disjoint Set

并查集是一种树型的数据结构,它记录一组元素,这些元素被划分为几个不相交(不重叠)的子集。换句话说,并查集是一组集合,其中没有项目可以在多个集合中。它也被称为Union-FInd,因为它支持对子集的联合和查找操作。

在并查集中,树表示一个集合,并查集中有多个集合,也就是有多棵树,每棵树存在数组中,用双亲表示法,不是左右孩子表示法

并查集的思想是用一个数组表示了整片森林(parent),树的根节点唯一标识了一个集合,我们只要找到了某个元素的的树根,就能确定它在哪个集合里。

An example

比如:比如有10个人要参加荒野求生大赛,各自来自不同的大学,起先互不相识,每个人都是一个独立的小团体,现给这些参赛者进行编号:{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; 给以下数组用来存储该小集体,数组中的数字代表:该小集体中具有成员的个数。

image-20220524135117302

10个元素一开始代表着10棵树的森林,一开始下标都是-1,接着让其中的一些树作为根节点

于是:比赛开始,假如按照喜欢的NBA球队来分组,喜欢不同球队的小分队一起上路
G.S.Warrior小分队s1={0,6,7,8},L.A.Lakers小分队s2={1,4,9},BostonCeltics小分队s3={2,3,5}就相互认识了,10个人形成了三个小团体。假设有三个leader(0,1,2)担任队长,负责大家的工作行程和安排。

image-20220524135123648

于是我们把剩下的成员下标改成了指向leader,这就通过数组实现了一个树形表示

只需要孩子把下标加等到父亲,然后把自己指向父亲下标就可以了,所以说存负数的就是根了,同时负数的值也可以同时得知这棵树有多少个值

image-20220524135923407

从上图可以看出:编号6,7,8同学属于0号小分队,该小分队中有4人(包含队长0);编号为4和9的同学属于1号小分队,该小分队有3人(包含队长1),编号为3和5的同学属于2号小分队,该小分队有3个人(包含队长1)。

由此我们可以发现如下规律:

🌮 数组的下标对应集合中元素的编号
🌮 数组中如果为负数,负号代表根,数字代表该集合中元素个数
🌮 数组中如果为非负数,代表该元素双亲在数组中的下标

此时,G.S.小队和L.A.小队在冒险时遇到了对方,于是两队决定一起探险

image-20220524141604965

于是现在开合并,0和4要合并,并不是这两个值合并,而是他们所在集合要合:分别找到两个值所在集合的根,再去合并,0所在集合的根是0,4所在集合的根是1

0和1合并可以选择直接把1的父亲用0,用0来做这棵树的根

应用

🍒 查找元素属于哪个集合沿着数组表示树形关系以上一直找到根(即:树中中元素为负数的位置)

🍒 查看两个元素是否属于同一个集合沿着数组表示的树形关系往上一直找到树的根,如果根相同表明在同一个集合,否则不在

🍒 将两个集合归并成一个集合将两个集合中的元素合并将一个集合名称改成另一个集合的名称

🍒 集合的个数遍历数组,数组中元素为负数的个数即为集合的个数。

模拟实现

因此这个数据结构的基本接口只包含三个操作:

  • make_set(v)- 创建一个由新元素组成的新集合v
  • union_sets(a, b)- 合并两个指定的集合(元素a所在的集合,元素所在的集合b
  • find_set(v)- 返回包含元素的集合的代表(也称为领导者)v。这个代表是其对应集合的一个元素。它由数据结构本身在每个集合中选择(并且可以随时间变化,即在union_sets调用之后)。该代表可用于检查两个元素是否属于同一集合。 a并且b完全在同一个集合中,如果find_set(a) == find_set(b)。否则它们在不同的集合中。
class UnionFindSet
{
public:
	UnionFindSet(size_t size)
		: _ufs(size, -1)
	{}

	bool Union(int x1, int x2)
	{
		int root1 = FindRoot(x1);
		int root2 = FindRoot(x2);
		// x1已经与x2在同一个集合
		if (root1 == root2)
			return false;
		
		// 将两个集合中元素合并
		_ufs[root1] += _ufs[root2];
		// 将其中一个集合名称改变成另外一个
		_ufs[root2] = root1;
		return true;
	}

	// 数组中负数的个数,即为集合的个数
	size_t Count()const
	{
		size_t count = 0;
		for (auto e : _ufs)
		{
			if (e < 0)
				++count;
		}
		return count;
	}

	// 给一个元素的编号,找到该元素所在集合的名称
	int FindRoot(int index)
	{
		assert(index < _ufs.size());
		// 如果数组中存储的是负数,找到,否则一直继续
		while (_ufs[index] >= 0)
		{
			index = _ufs[index];
		}
		return index;
	}
private:
	vector<int> _ufs;
};

小试牛刀

并查集可以有时候简化题目

剑指 Offer II 116. 省份数量

有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。

省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。

给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。

返回矩阵中 省份 的数量。

image-20220524143924084

分析示例1:

image-20220524144400133

借助并查集的关系,把互相之间有关系的划分到一个集合,其中为负数的代表根,最后就能看出有2棵树,那么这道题目也就很简单了

 int findCircleNum(vector<vector<int>>& isConnected) {
        UnionFindSet ufs(isConnected.size());
        for(int i=0;i<isConnected.size();i++)
        {
            for(int j=0;j<isConnected.size();j++)
            {//合并
                if(isConnected[i][j]==1)
                {
                    ufs.Union(i,j);
                }
            }
        }
        return ufs.Size();
    }

image-20220524150721777

990. 等式方程的可满足性

给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:“a==b” 或 “a!=b”。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。

只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。

image-20220524150807502

这道题也可以用并查集来做,相等具有传递性,合并到一个集合,不相等的不能判断他们在一个集合

我们可以判断两遍,第一次只要判断相等的,第二次再判断不相等的,看看不在一个集合的会不会和在一个集合的冲突就可以了

建立映射

由于之前的并查集是只能接收整数,所以我们对于小写字母可以进行映射,把[a-z]26个英文字母,将他们进行[0,25]一一对应映射,这个问题同样可能发生在上一题省份,如果给的不是下标,而是字符串,省份名,就需要建立映射关系

复杂问题我们可以试着用vector+map来解决映射问题

这种简单的纯小写字母可以直接用数组映射

题解
    bool equationsPossible(vector<string>& equations) {
        UnionFindSet ufs(26);//a-z
        //第一遍把相等的值和到一个集合
        for(auto& str:equations)
        {
            if(str[1]=='=')
            {
                ufs.Union(str[0]-'a',str[3]-'a');
            }
        }
        //第二遍,把不相等的值判断在不在一个集合,在就逻辑相悖,不合法,返回false
        for(auto& str:equations)
        {
            if(str[1]=='!')
            {
                if(ufs.FindRoot(str[0]-'a')==ufs.FindRoot(str[3]-'a'))
                {
                    return false;
                }
            }
        }
        return true;
    }

image-20220524154126231

路径合并

然而,上面这种实现是低效的。很容易构造一个例子,让树退化成长链。

此优化旨在加快find_set.

如果我们调用find_set(v)某个顶点v,我们实际上会找到p我们在路径上访问的所有顶点v和实际代表之间的代表p。诀窍是通过将每个访问顶点的父节点直接设置为 来缩短所有这些节点的路径p

您可以在下图中看到该操作。左边是一棵树,右边是调用后的压缩树find_set(7),缩短了访问节点 7、5、3、2 的路径。

调用 find_set(7) 的路径压缩

具体可以参考下面的网站学习https://cp-algorithms.com/data_structures/disjoint_set_union.html#naive-implementation

LRU Cache

LRU是(Least Recently Used)的缩写,意思是最近最少使用,它是一种Cache替换算法。操作系统中的很多算法都是采用了LRU的思想。

我们知道Cache的容量有限,因此当Cache的容量用完后,而又有新的内容需要添加进来时, 就需要挑选并舍弃原有的部分内容,从而腾出空间来放新内容。LRU Cache 的替换原则就是将最近最少使用的内容替换掉。其实,LRU译成最久未使用会更形象, 因为该算法每次替换掉的就是一段时间内最久没有使用过的内容。

image-20220525093848071

实现LRUCache

image-20220525192626663

实现LRU Cache的方法和思路很多,但是要保持高效实现O(1)的put和get,那么使用双向链表和哈希表的搭配是最高效和经典的。使用双向链表是因为双向链表可以实现任意位置O(1)的插入和删除,使用哈希表是因为哈希表的增删查改也是O(1)。

146. LRU 缓存

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。

实现 LRUCache 类:

🌮 LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
🌮 int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
🌮 void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

image-20220525102640256

分析

分析一下示例就可以得出LRU的机制

image-20220525150231257

下面还要分析一下get的影响

image-20220525155305159

实现
  1. O(1)

由于查找效率需要能够达到O(1)所以说只能用到HashMap,作为一个底层的数据结构

  1. LRUCache

由于get可能需要在整个数据的中部删除一个数据,所以说为了保证效率,我们不能使用数组,只有链表可以做到这个效果

private:
    list<pair<int,int>>  _LRUList;//插入删除
    unordered_map<int ,int> _kvMap;//找值

仅仅这两个结构是不足够的因为达不到O(1),因为虽然map能找到位置,但是插入删除还是效率不够,找到值是O(1),虽然知道了key对应的在Map的位置,但是还需要调整List中使顺序到最后去,此时还不知道key在List的位置,所以我们想到了map的value存的是list的迭代器

private:
    list<pair<int,int>>  _LRUList;//插入删除
    unordered_map<int ,list<pair<int,int>>::iterator> _kItMap;//找值

此时get我们的key的时候会得到在list中存的位置的迭代器,既可以拿到value,又可以调整list中节点的顺序位置

class LRUCache {
    typedef list<pair<int,int>> LRULIST;
    typedef list<pair<int,int>>::iterator LRULIST_IT;
public:
    LRUCache(int capacity) {
        _capacity=capacity;
    }
    
    int get(int key) {
        auto mIt=_kItMap.find(key);
        if(mIt == _kItMap.end())
        {
            return -1;
        }
        else
        {
            LRULIST_IT ltIt=mIt->second;
            int value=ltIt->second;

            //调整顺序
             _LRUList.splice(_LRUList.end(),_LRUList,ltIt);

             return value;
        }
    }
    
    void put(int key, int value) {
        //如果不在cache,容量不满,就插入
        auto mIt= _kItMap.find(key);
        if(mIt == _kItMap.end())
        {   
            if(_LRUList.size()>=_capacity)
            {
                //容量满了,删除头
                _kItMap.erase(_LRUList.front().first);
                _LRUList.pop_front();
            }
            _LRUList.push_back(make_pair(key,value));//尾插
            _kItMap[key] = --_LRUList.end(); //list最后一个元素的迭代器
        }
        //如果在就更新一下
        else
        {
            //更新
            LRULIST_IT ltIt=mIt->second;
            ltIt->second=value;
            //调整顺序
            //method1:保存list中的值,然后尾插一个新的,然后更新map
            //method2: splice
            _LRUList.splice(_LRUList.end(),_LRUList,ltIt);
        }
    }

    private:
    list<pair<int,int>>                                _LRUList;//插入删除
    unordered_map<int ,list<pair<int,int>>::iterator>  _kItMap;//找值
    int                                                _capacity;
};

image-20220525192301295

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

言之命至9012

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值