数据结构
背景
我们知道Java中HashMap是一个无序的数据结构,因为其键值对<key,value>中key的底层实现是一个set,所以HashMap在遍历的时候,元素的遍历顺序和插入顺序是不一致的。在一些情况下,我们需要一种数据结构兼具Hash性以及有序性,这个时候就会需要使用到新的数据结构了–TreeMap
在leetcode刷题写每日一题的时候,在学习题解的过程中,了解到了TreeMap(其实之前的时候就有遇到过,不过没有细致研究),所以这次做好笔记,希望对以后的学习有帮助(下面的题解是参考灵茶山艾府的解答)
https://leetcode.cn/problems/count-integers-in-intervals/description/
1.Java中的TreeMap
1.1TreeMap和HashMap的区别
-
内部实现:
HashMap使用哈希表(Hash Table)实现,通过哈希算法将键映射到存储桶(bucket)上。
TreeMap使用红黑树(Red-Black Tree)实现,它是一种自平衡的二叉搜索树。 -
排序:
HashMap不会对键进行排序,因此键的顺序是不确定的。
TreeMap会对键进行排序,它根据键的自然顺序或者通过Comparator进行排序。 -
性能:
HashMap的插入、删除和查找操作的平均时间复杂度为O(1),具有常数级别的性能。
TreeMap的插入、删除和查找操作的时间复杂度为O(log n),其中n是元素的数量。由于红黑树的自平衡特性,TreeMap的性能相对较稳定。 -
迭代顺序:
HashMap的迭代顺序是不确定的,它与键的插入顺序无关。
TreeMap的迭代顺序是按照键的顺序进行的,可以通过自然顺序或者Comparator进行控制。 -
null键和null值:
HashMap允许null键和null值,可以存储一个null键和多个null值。
TreeMap不允许null键,但允许null值。
1.2TreeMap具体细节
TreeMap中有许多具体的内容,内容太多了,可以仔细学习下面这篇博文。
https://blog.csdn.net/qq_45344586/article/details/129727558
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
public class TreeMap_Learning {
public static void main(String[] args) {
TreeMap<String, Integer> map = new TreeMap<>();
// 向 TreeMap 中添加键值对
map.put("Alice", 90);
map.put("Bob", 80);
map.put("Charlie", 70);
map.put("David", 60);
//================TreeMap的遍历输出(按照自然顺序输出)====================
for(Map.Entry<String, Integer> entry : map.entrySet()){
System.out.println(entry.getKey()+":"+entry.getValue());
}
//==============实现TreeMap按照key值逆序:创建一个新的TreeMap,并传入逆序的Comparator==========
TreeMap<String, Integer> reverseTreeMap = new TreeMap<>(Comparator.reverseOrder());
// 将原始TreeMap中的键值对逐个复制到新的TreeMap中
reverseTreeMap.putAll(map);
// 遍历新的TreeMap,按照逆序输出键值对
for (String key : reverseTreeMap.keySet()) {
System.out.println(key + ": " + reverseTreeMap.get(key));
}
// 根据键获取对应的值
System.out.println(map.get("Alice")); // 90
System.out.println(map.get("Eve")); // null
// 根据键删除对应的键值对
map.remove("Bob");
System.out.println(map); // {Alice=90, Charlie=70, David=60}
// 获取 TreeMap 中的键值对的数量
System.out.println(map.size()); // 3
// ===================判断 TreeMap 中是否包含某个键=================
System.out.println(map.containsKey("Charlie")); // true
System.out.println(map.containsKey("Eve")); // false
// ===========判断 TreeMap 中是否包含某个值===================
System.out.println(map.containsValue(70)); // true
System.out.println(map.containsValue(50)); // false
// 清空 TreeMap 中的所有键值对
//map.clear();
//System.out.println(map);
//==================相较于HashMap多出的一些操作=================
// 初始化的TreeMap为{Alice=90, Bob=80, Charlie=70, David=60}
// 返回 TreeMap 中最小和最大的键
System.out.println(map.firstKey()); // Alice
System.out.println(map.lastKey()); // David
// 返回 TreeMap 中小于和大于给定键的最近的键
System.out.println(map.lowerKey("Bob")); // Alice
System.out.println(map.higherKey("Bob")); // Charlie
// 返回 TreeMap 中小于等于和大于等于给定键的最近的键
System.out.println(map.floorKey("Bob")); // Bob
System.out.println(map.ceilingKey("Bob")); // Bob
// 返回一个子映射,包含给定范围内的所有键值对
System.out.println(map.subMap("Alice", true, "Charlie", true)); // {Alice=90, Bob=80, Charlie=70}
// 返回一个子映射,包含小于或等于给定键或大于或等于给定键的所有键值对
System.out.println(map.headMap("Charlie", true)); // {Alice=90, Bob=80, Charlie=70}
System.out.println(map.tailMap("Charlie", true)); // {Charlie=70, David=60}
// 返回 TreeMap 中最小和最大的键值对
System.out.println(map.firstEntry()); // Alice=90
System.out.println(map.lastEntry()); // David=60
// 返回 TreeMap 中小于和大于给定键的最近的键值对
System.out.println(map.lowerEntry("Bob")); // Alice=90
System.out.println(map.higherEntry("Bob")); // Charlie=70
// 返回 TreeMap 中小于等于和大于等于给定键的最近的键值对
System.out.println(map.floorEntry("Bob")); // Bob=80
System.out.println(map.ceilingEntry("Bob")); // Bob=80
// 返回并删除 TreeMap 中最小和最大的键值对
System.out.println(map.pollFirstEntry()); // Alice=90
System.out.println(map.pollLastEntry()); // David=60
}
}
1.3题解
class CountIntervals {
TreeMap<Integer, Integer> m = new TreeMap<>();
int cnt; // 所有区间长度和
public CountIntervals() {}
public void add(int left, int right) {
// 遍历所有被 [left,right] 覆盖到的区间(部分覆盖也算)
for (var e = m.ceilingEntry(left); e != null && e.getValue() <= right; e = m.ceilingEntry(left)) {
int l = e.getValue(), r = e.getKey();
left = Math.min(left, l); // 合并后的新区间,其左端点为所有被覆盖的区间的左端点的最小值
right = Math.max(right, r); // 合并后的新区间,其右端点为所有被覆盖的区间的右端点的最大值
cnt -= r - l + 1;
m.remove(r);
}
cnt += right - left + 1;
m.put(right, left); // 所有被覆盖到的区间与 [left,right] 合并成一个新区间
}
public int count() { return cnt; }
}
2.Python中的SortedDict
2.1 SortedDict和dict
Python中map实现是dict(字典),不过python 3.7之前的dict是无序的,但是之后dict就是有序的(按照插入的顺序)。在本题的解法中使用到了SortedDict(),这个实现方式通常是基于红黑树或者跳表等数据结构,以实现有序性。SortedDict的插入、删除和查找操作的时间复杂度通常为O(log n),其中n是元素的数量。由于需要维护有序性,相对于普通的字典,SortedDict的操作可能会稍慢一些。dict的插入、删除和查找操作的平均时间复杂度为O(1),具有常数级别的性能。SortedDict需要使用sortedcontainers库,因此需要额外安装该库。dict是Python标准库中的内置类型,无需额外安装。
2.2 SortedDict的常见操作
SortedDict的内容可以在下面的博文中学习:
https://blog.csdn.net/hgq522/article/details/122375627
# 导入模块库
from sortedcontainers import SortedDict
# 初始化
sorted_dict = SortedDict({1: 'a', 4: 'd', 2: 'b'})
# 打印整个sorted-list
print('sorted dict is: ', sorted_dict)
# 增加一个元素
sorted_dict[3] = 'c'
# 再次打印整个sorted-list
print('sorted dict after adding an element: ', sorted_dict)
# 获取key list
print('get the key list', sorted_dict.keys())
# 获取最小key
print('get the min key', sorted_dict.keys()[0])
# 获取最大key
print('get the max key', sorted_dict.keys()[-1])
# 删除最大key
maxKey = sorted_dict.keys()[-1]
sorted_dict.pop(maxKey)
# 再次打印整个sorted-list
print('sorted dict after adding an element: ', sorted_dict)
# 删除所有
sorted_dict.clear()
print('sorted dict after removing all elements: ', sorted_dict)
SortedList 的文档
https://grantjenks.com/docs/sortedcontainers/sortedlist.html#sortedcontainers.SortedList.bisect_left
2.3 Python解法
from sortedcontainers import SortedDict
class CountIntervals:
def __init__(self):
self.d = SortedDict()
self.cnt = 0 # 所有区间长度和
def add(self, left: int, right: int) -> None:
# 遍历所有被 [left,right] 覆盖到的区间(部分覆盖也算)
i = self.d.bisect_left(left)
while i < len(self.d) and self.d.values()[i] <= right:
r, l = self.d.items()[i]
left = min(left, l) # 合并后的新区间,其左端点为所有被覆盖的区间的左端点的最小值
right = max(right, r) # 合并后的新区间,其右端点为所有被覆盖的区间的右端点的最大值
self.cnt -= r - l + 1
self.d.popitem(i)
self.cnt += right - left + 1
self.d[right] = left # 所有被覆盖到的区间与 [left,right] 合并成一个新区间
def count(self) -> int:
return self.cnt
3.C++中的Map
3.1 C++中的有序关联容器和无序关联容器
3.2 C++中map的操作
具体的内容可以查看一下博文
https://blog.csdn.net/sinat_31608641/article/details/107424059
1.查找元素
//头文件
#includen <map>
map<int, string> ID_Name;
int main()
{
std::map<char, int> mymap;
mymap['a'] = 50;
mymap['b'] = 100;
mymap['c'] = 150;
mymap['d'] = 200;
std::map<char, int>::iterator it = mymap.find('b'); //查找值
if (it != mymap.end()) {
mymap.erase(it); // 删除某个值
}
return 0;
}
2.插入元素
#include <iostream>
#include <map>
int main()
{
std::map<char, int> mymap;
// 插入单个值
mymap.insert(std::pair<char, int>('a', 100));
mymap.insert(std::pair<char, int>('z', 200));
//返回插入位置以及是否插入成功
std::pair<std::map<char, int>::iterator, bool> ret;
ret = mymap.insert(std::pair<char, int>('z', 500));
if (ret.second == false) {
std::cout << "element 'z' already existed";
std::cout << " with a value of " << ret.first->second << '\n';
}
//指定位置插入
std::map<char, int>::iterator it = mymap.begin();
mymap.insert(it, std::pair<char, int>('b', 300)); //效率更高
mymap.insert(it, std::pair<char, int>('c', 400)); //效率非最高
//范围多值插入
std::map<char, int> anothermap;
anothermap.insert(mymap.begin(), mymap.find('c'));
// 列表形式插入
anothermap.insert({ { 'd', 100 }, {'e', 200} });
return 0;
}
3.取值操作
Map中元素取值主要有at和[ ]两种操作,at会作下标检查,而[]不会
map<int, string> ID_Name;
//ID_Name中没有关键字2016,使用[]取值会导致插入
//因此,下面语句不会报错,但打印结果为空
cout<<ID_Name[2016].c_str()<<endl;
//使用at会进行关键字检查,因此下面语句会报错
ID_Name.at(2016) = "Bob";
4.容量查询
// 查询map是否为空
bool empty();
// 查询map中键值对的数量
size_t size();
// 查询map所能包含的最大键值对数量,和系统和应用库有关。
// 此外,这并不意味着用户一定可以存这么多,很可能还没达到就已经开辟内存失败了
size_t max_size();
// 查询关键字为key的元素的个数,在map里结果非0即1
size_t count( const Key& key ) const; //
5.交换
// 就是两个map的内容互换
void swap( map& other );
6.删除
// 删除迭代器指向位置的键值对,并返回一个指向下一元素的迭代器
iterator erase( iterator pos )
// 删除一定范围内的元素,并返回一个指向下一元素的迭代器
iterator erase( const_iterator first, const_iterator last );
// 根据Key来进行删除, 返回删除的元素数量,在map里结果非0即1
size_t erase( const key_type& key );
// 清空map,清空后的size为0
void clear();
3.3 C++题解
class CountIntervals {
map<int, int> m;
int cnt = 0; // 所有区间长度和
public:
CountIntervals() {}
void add(int left, int right) {
// 遍历所有被 [left,right] 覆盖到的区间(部分覆盖也算)
for (auto it = m.lower_bound(left); it != m.end() && it->second <= right; m.erase(it++)) {
int l = it->second, r = it->first;
left = min(left, l); // 合并后的新区间,其左端点为所有被覆盖的区间的左端点的最小值
right = max(right, r); // 合并后的新区间,其右端点为所有被覆盖的区间的右端点的最大值
cnt -= r - l + 1;
}
cnt += right - left + 1;
m[right] = left; // 所有被覆盖到的区间与 [left,right] 合并成一个新区间
}
int count() { return cnt; }
};