Set、Map和HashMap
前言
本篇博客将根据现有知识对数据结构Set与Map做以小结,以下博客仅作为个人学习过程的小结,如能对各位博友有所帮助不胜荣幸。
本篇博客将简单介绍Set与Map的概念原理应用,以及各自特点及区别,本篇只做本人小结,后期随学习深入再做补充修改。
概念和背景
搜索
搜索即查找是数据结构中经常使用的一种做法,常用到的搜索方法有 直接遍历 和 二分查找
直接遍历的时间复杂度为O(n),但元素过多时执行效率会比较慢,而二分查找的时间复杂度为O(log(n)),效率较直接遍历有明显的提高,但前提必须是查找区间有序
上述的查找比较适合静态查找,即查找过程中不会读元素进行删改,而另一方面我们有时操作时有需要通过查询一个数据来得到与它关联的另一个数据,还有时需要对数据查找的同时进行一些修改操作,即动态查找
而上述两种方式显然就无法满足要求,Java中为我们提供了Set和Map两种数据结构,主要来应对所需要的动态查找
key-value模型
一般我们把要所搜索的数据称为关键字(key),把和关键字关联对应的数据称为(value),将其整体称为key-value键值对,因此就引申出两种模型:纯key模型和key-value模型
纯key模型:即我们要通过关键字key来查找,如查找一堆数据中是否存在key=0的数据
key-value模型:通过key关键字来查找与其向关联的数据是否存在,如在一个用户类中,通过其定义的一个对象的名字查找到该对象的年龄是多少
Set就是单纯值存key,用于纯key模型查找
Map存储的就是一个个key-value键值对,用于key-value模型查找
Set
说明
- Set是继承自Collection的一个接口
- 其内部值存储一种数据类型,即key,并且内部不能存入重复元素
- Set最常用的功能就是对集合中元素去重
- Set中key的值不可修改,若要改需要将原来的key删掉,修改再重新添加
- Sey不能存入 key = null 的key
- Set接口的实现类常用有两种,TreeSet和HashSet,而LinkedHashSet它是在HashSet的基础上维护了一个双向链表来记录元素的插入次序
- Set的底层是Map来实现的,其使用key和Object的一个默认对象作为键值对存入Map中
TreeSet和HashSet的区别
常用方法
Map
Map是一个接口,没有继承其他接口,其内部储存的一个个 key-value 键值对,并且key总是唯一的
Map的说明
- Map是一个接口,不能直接实例对象,只能实例化它的实现类TreeMap和HashMap
- Map中存放的key一定时唯一的,但value可以重复
- Map中插入key,key不能为null,但value可以
- Map中的所有key可以被抽离出来放到一个 Set 中,即调用其 keySet() 方法
- Map.Entry<K,V> 是Map的一个内部类,用来存放key-value的映射关系,给类中提供了key和v,alue的获取方法,已经value的设置方法
TreeMap与HashMap的区别
常用方法
二叉搜索树(Binary Search Tree)
概念:
二叉搜索树,又叫二叉排序树,它是一棵空树或者满足如下特征的二叉树
- 若它的左子树不为空,则左子树的所有节点的值都小于根节点
- 若他的右子树不为空,则右子树的所有节点的值都大于根节点
- 它的左右子树也满足以上规律,同样是一棵二叉搜索树
如何对二叉搜索树进行查找操作
查找 val
如果根节点不为空
- 如果 val == root.val,则找到结点,返回root
- 如果 val > root.val,则root = root.left,在左子树中找
- 如果 val < root.val,则root = root.right,在右子树中找
如果节点为空,则返回null,未找到
如何对二叉搜索树进行插入操作
插入val
如果数为空,直接在根节点插入val
否则,按照查找逻辑确定 val 的插入位置,定义一个父节点parent = null
- 如果 val == root.val,树中已有该值的节点,不允许再次插入
- 如果 val > root.val,parent = root,root = root.right
- 如果 val < root.val,parent = root,root = root.left
直到root等于空,此时,如果(val > parent.val),parent.right = null 否则 parent.left = null
如何对二叉搜索树进行删除操作
如果直接删除一个二叉搜索树的节点,可能会导致树的结构倒塌,因此对于二叉搜索树的删除需要分如下情况讨论:
通过要删除的 val 值,查找到要的删除结点 cur,其父节点为 parent
-
cur.left = null
1.cur 是根节点root,则root = root.right
2.cur 不是根节点 而是 parent.left,则parent.left = cur.right
3.cur 不是根节点 而是 parent.right,则parent.right = cur.right -
cur.right = null
1.cur 是根节点root,则root = root.left
2.cur 不是根节点 而是 parent.left,则parent.left = cur.left
3.cur 不是根节点 而是 parent.right,则parent.right = cur.left -
cur.left != null && cur.right != null
此处需要使用替换法,在它的右子树中找到其中序遍历的一个节点(即值最小的节点),将它替换到要删除结点的位置
具体实现
public class BinarySearchTree {
public static class TreeNode {
public int key;
public TreeNode left;
public TreeNode right;
public TreeNode(int key) {
this.key = key;
}
}
private TreeNode root = null;
/**
* 在搜索树中查找 key,如果找到,返回 key 所在的结点,否则返回 null
*
* @param key
* @return null 表示没有查询到,非null表示查询到该节点
*/
public TreeNode search(int key) {
TreeNode node = root;
while (node != null) {
if (key == node.key) {
return node;
} else if (key > node.key) {
node = node.right;
} else {
node = node.left;
}
}
return null;
}
/**
* 插入
*
* @param key
* @return true 表示插入成功, false 表示插入失败
*/
public boolean insert(int key) {
if(root == null){
root = new TreeNode(key);
return true;
}
TreeNode parent = null;
TreeNode node = root;
while(node != null){
if(key == node.key){
return false;
}else if(key < node.key){
parent = node;
node = node.left;
}else {
parent = node;
node = node.right;
}
}
if(key < parent.key){
parent.left = new TreeNode(key);
}else{
parent.right = new TreeNode(key);
}
return true;
}
/**
* 删除
*
* @param key
* @return true成功返回 ,false失败返回
*/
public boolean remove(int key) {
TreeNode parent = null;
TreeNode cur = root;
while (cur != null) {
if (key == cur.key) {
break;
} else if (key > cur.key) {
parent = cur;
cur = cur.right;
} else {
parent = cur;
cur = cur.left;
}
}
if(cur == null){
return false;
}
if(parent == null){
TreeNode n = cur.right;
while(n.left != null){
n = n.left;
}
n.left = cur.left;
return true;
}
if(cur.left == null && cur.right == null){
if(key < parent.key){
parent.left = null;
}else{
parent.right = null;
}
return true;
}
if(cur.left != null && cur.right != null){
TreeNode node = cur.right;
while(node.left != null){
node = node.left;
}
node.left = cur.left;
node.right = cur.right;
if(key < parent.key){
parent.left = node;
}else{
parent.right = node;
}
return true;
}
if(cur.left == null){
if(cur == root){
root = root.right;
}else if(cur == parent.left){
parent.left = cur.right;
}else if(cur == parent.right){
parent.right = cur.right;
}
return true;
}
if(cur == root){
root = root.left;
}else if(cur == parent.left){
parent.left = cur.left;
}else if(cur == parent.right){
parent.right = cur.left;
}
return true;
}
}
性能
二叉搜索树的插入和删除执行的快慢很大一部分取决于查找的执行效率
二叉搜索树的的查找操作,其实就是对树的遍历操作其执行的小率与其高度有关,而二叉搜索树由于插入元素的先后次序不同,导致最后得到的树形就不同
最理想情况:完全二叉树,时间复杂度O(log(n))
最坏情况:单枝树,时间复杂度O(n)
与Java集合类的关系
Java中利用搜索树实现的 TreeSet 和 TreeMap,为了防止搜索树在操作的过程中退化成单枝树,实际底层利用红黑树实现
红黑树是一棵近似平和的二叉搜索树,它在二叉搜索树的基础上+颜色已经红黑树自身性质约束,是红黑树在操作时总能近似的是一棵平衡的二叉搜索树
TreeSet和TreeMap
- TreeSet 和 TreeMap是Java中利用二叉搜索树来实现的 Map 和 Set
- TreeSet 和 TreeMap底层用搜索树(红黑树)实现,所以其存储对于key是有序的
- TreeSet 和 TreeMap 的添加/删除/查找操作时间复杂度都是O(log(n))
- TreeSet 和 TreeMap 的方法都是不同步的,即线程不安全
以上便是对七大Set、Map的知识点小结,随着后续学习的深入还会同步的对内容进行补充和修改,如能帮助到各位博友将不胜荣幸,敬请斧正