边学边记——数据结构☞和搜索有关的数据结构(搜索树,Set,Map,哈希表)

news/2024/10/17 22:12:22/

目录

模型

一、搜索

1. 场景

2. 搜索树

2.1 概念

2.2 查找

2.3 插入

2.4 删除

2.5 实现 

2.6 性能分析

2.7 和Java的关系

二、Set

1. 常见方法

2. 注意

三、Map

1. 关于Map.Entry的说明,>

2. Map的常用方法说明

3. 注意

四、哈希表

1. 概念

2. 冲突

2.1 概念

2.2 避免

3. 降低 / 解决冲突的办法

3.1 降低冲突的办法(提前准备)

3.2 降低冲突的办法(解决冲突)

4. 冲突严重时的解决办法

5. 实现

6. 性能分析

7. 和java类集的关系


模型

        一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value的键值对,所以模型会有两种:

  • 纯key模型:Set接口(集合)
  • key-value模型:Map接口(映射)

一、搜索

1. 场景

(1). 静态的有序数组

二分查找——支持随机访问+有序

查找快,插入删除慢,所以只适合静态数据集(数据变化很少的情况)

(2). 搜索树

平衡搜索树:

AVL树/红黑树(内存),B-树系列(B+树),R-树系列

(3). 哈希表

2. 搜索树

2.1 概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树

如图: 

 特点:中序遍历的结果是有序的。

2.2 查找

若根节点不为空:

  • 如果根节点key == 查找key,返回true。
  • 如果根节点key > 查找key,在其左子树查找。
  • 如果根节点key < 查找key,在其右子树查找。

否则,返回false。

2.3 插入

  • 如果树为空树,即 根 == null,直接插入。
  • 如果树不是空树,安装查找逻辑确定插入位置,插入新结点。

2.4 删除

设待删除结点为 cur, 待删除结点的双亲结点为 parent

1. cur.left == null

  • cur 是 root,则 root = cur.right
  • cur 不是 root,cur 是 parent.left,则 parent.left = cur.right
  • cur 不是 root,cur 是 parent.right,则 parent.right = cur.right

2. cur.right == null

  • cur 是 root,则 root = cur.left
  • cur 不是 root,cur 是 parent.left,则 parent.left = cur.left
  • cur 不是 root,cur 是 parent.right,则 parent.right = cur.left

3. cur.left != null && cur.right != null

  • 需要使用替换法进行删除,即在它的右子树中寻找中序下的第一个结点(关键码最小)(也可以是左子树中最大的),用它的值填补到被删除节点中,再来处理该结点的删除问题。 

2.5 实现 

public class BinarySearchTree {public static class Node {int key;Node left;Node right;public Node(int key) {this.key = key;}}private Node root = null;//查找的操作public Node search(int key) {Node cur = root;while (cur != null) {if (key == cur.key) {return cur;} else if (key < cur.key) {cur = cur.left;} else {cur = cur.right;}}return null;}//插入的操作//插入过程中可能会失败(key重复了)public boolean insert(int key) {if (root == null) {root = new Node(key);return true;}Node cur = root;//记录双亲结点Node parent = null;while (cur != null) {if (key == cur.key) {//key重复return false;} else if (key < cur.key) {parent = cur;cur = cur.left;} else {parent = cur;cur = cur.right;}}//cur == nullNode node = new Node(key);if (key < parent.key) {parent.left = node;} else {parent.right = node;}return true;}//删除的操作public boolean remove(int key) {Node cur = root;Node parent = null;while (cur != null) {if (key == cur.key) {break;} else if (key < cur.key) {parent = cur;cur = cur.left;} else {parent = cur;cur = cur.right;}}// 该元素不在二叉搜索树中if(null == cur){return false;}/* 根据cur的孩子是否存在分四种情况1. cur左右孩子均不存在2. cur只有左孩子3. cur只有右孩子4. cur左右孩子均存在看起来有四种情况,实际情况1可以与情况2或者3进行合并,只需要处理是那种情况即可除了情况4之外,其他情况可以直接删除情况4不能直接删除,需要在其子树中找一个替代节点进行删除*/if(cur.left == null){   //3.if(cur == root){    //parent == nullroot = cur.right;}else if(parent.left == cur){parent.left = cur.right;}else{//parent.right == cur;parent.right = cur.right;}}else if(cur.right == null){    //2.if(cur == root){root = cur.left;}else if(parent.left == cur){parent.left = cur.left;}else{parent.right = cur.left;}}else{  //4.//cur.left != null && cur.right != null//替换删除,此处选择右子树中最小的一个(即右子树中最左的一个)Node toDeleteParent = cur;Node toDelete = cur.right;while(toDelete.left != null){toDeleteParent = toDelete;toDelete = toDelete.left;}//此时toDelete是我们要删除的结点//先把值替换cur.key = toDelete.key;//删除toDelete//需要判断此时是toDelete左孩子还是右孩子//如果cur没有左孩子,那么此时toDelete就是cur的右孩子,此时toDeleteParent == curif(toDeleteParent.left == toDelete){toDeleteParent.left = toDelete.right;}else{//tpDeleteParent.right == toDeletetoDeleteParent.right = toDelete.right;}}return true;}
}

2.6 性能分析

        插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。

        对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

        但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

  • 最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:log_{2}N
  • 最差情况下,二叉搜索树退化为单支树,其平均比较次数为:\frac{N}{2}

2.7 和Java的关系

        TreeMap 和 TreeSet 即 java 中利用搜索树实现的 Map 和 Set;实际上用的是红黑树,而红黑树是一棵近似平衡的二叉搜索树,即在二叉搜索树的基础之上 + 颜色以及红黑树性质验证。 

二、Set

Set官方文档

        Set与Map主要的不同有两点:Set是继承自Collection的接口类,Set中只存储了Key。

1. 常见方法

方法解释
boolean add(E e)添加元素,但重复元素不会被添加成功
void clear()清空集合
boolean contains(Object o)判断 o 是否在集合中
Iterator<E> iterator()返回迭代器
boolean remove(Object o)删除集合中的 o
int size()返回set中元素的个数
boolean isEmpty()检测set是否为空,空返回true,否则返回false
Object[] toArray()将set中的元素转换为数组返回
boolean containsAll(Collection<?> c)集合c中的元素是否在set中全部存在,是返回true,否则返回
false
boolean addAll(Collection<? extends
E> c)
将集合c中的元素添加到set中,可以达到去重的效果

2. 注意

  • Set是继承自Collection的一个接口类。
  • Set中只存储了key,并且要求key一定要唯一。
  • Set的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中。
  • Set最大的功能就是对集合中的元素进行去重。
  • 实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础上维护了一个双向链表来记录元素的插入次序。
  • Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入。
  • Set中不能插入null的key。

TreeSet和HashSet的区别:

Set底层结构TreeSetHashSet
底层结构红黑树哈希桶
插入/删除/查找时间
复杂度
O(1)
是否有序关于Key有序不一定有序
线程安全不安全不安全
  • 插入/删除/查找区别
按照红黑树的特性来进行插入和删除1. 先计算key哈希地址 2. 然后进行
插入和删除
比较与覆写key必须能够比较,否则会抛出
ClassCastException异常
自定义类型需要覆写equals和
hashCode方法
应用场景需要Key有序场景下Key是否有序不关心,需要更高的
时间性能

三、Map

Map官方文档

Map是一个接口类,该类没有继承自Collection,该类中存储的是<K,V>结构的键值对,并且K一定是唯一的,不能重复。

1. 关于Map.Entry<K, V>的说明

        Map.Entry<K, V> 是Map内部实现的用来存放<key, value>键值对映射关系的内部类,该内部类中主要提供了<key, value>的获取,value的设置以及Key的比较方式。

方法解释
K getKey()返回 entry 中的 key
V getValue()返回 entry 中的 value
V setValue(V value)将键值对中的value替换为指定value

注意:Map.Entry<K,V>并没有提供设置Key的方法。

2. Map的常用方法说明

方法解释
V get(Object key)返回 key 对应的 value
V getOrDefault(Object key, V defaultValue)返回 key 对应的 value,key 不存在,返回默认值
V put(K key, V value)设置 key 对应的 value
V remove(Object key)删除 key 对应的映射关系
Set<K> keySet()返回所有 key 的不重复集合
Collection<V> values()返回所有 value 的可重复集合
Set<Map.Entry<K, V>> entrySet()返回所有的 key-value 映射关系
boolean containsKey(Object key)判断是否包含 key
boolean containsValue(Object value)判断是否包含 value

3. 注意

  • Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap。
  • Map中存放键值对的Key是唯一的,value是可以重复的
  • Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复)。
  • Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。
  • Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入。
  • TreeMap和HashMap的区别:
    Map底层结构TreeMapHashMap
    底层结构红黑树哈希桶
    插入/删除/查找时间
    复杂度
    O(1)
    是否有序关于Key有序无序
    线程安全不安全不安全
    插入/删除/查找区别需要进行元素比较通过哈希函数计算哈希地址
    比较与覆写key必须能够比较,否则会抛出
    ClassCastException异常
    自定义类型需要覆写equals和
    hashCode方法
    应用场景需要Key有序场景下Key是否有序不关心,需要更高的
    时间性能

四、哈希表

        哈希表:固定长度的数组(顺序表),也是一种搜索的数据结构。

        顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(log_{2}N ),搜索的效率取决于搜索过程中元素的比较次数。

        理想的搜索方法:可以不经过任何比较一次直接从表中得到要搜索的元素

        如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

1. 概念

当向该结构中:

  • 插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
  • 搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。

        该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)

例:

数据集合{1,7,6,4,5,9};

哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。



 但是这种方式可能会产生冲突(碰撞),称之为哈希冲突(哈希碰撞)。 

2. 冲突

2.1 概念

        对于两个数据元素的关键字 和 (i != j),有 != ,但有:Hash( ) == Hash( ),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

        把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

2.2 避免

        哈希冲突是必然的,无法避免的。

虽然冲突是不可避免的,但是要想办法降低冲突率。

3. 降低 / 解决冲突的办法

3.1 降低冲突的办法(提前准备)

3.1.1 哈希函数的设计——除留余数法

常见哈希函数:直接定制法,除留余数法,平方取中法,折叠法,随机数法,数学分析法。

一般我们使用——除留余数法:

        设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:

        Hash(key) = key% p(p<=m),将关键码转换成哈希地址。

3.1.2 负载因子调节

散列表的载荷因子定义为:\alpha = 填入表中的元素个数 / 散列表的长度

降低冲突率——设定一个阈值,当负载因子超过阈值时,进行数组的扩容。

3.2 降低冲突的办法(解决冲突)

3.2.1 封闭区间中解决(闭散列)——线性探测和二次探测

a. 线性探测

例:数据集合{1,7,6,4,5,9,44};

        比如在之前的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,下标为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

当插入44时,hash(44) = 44 % 4 = 4,但是4已经被占用,所以只能继续往后放,找到后面的第一个空位置即8位置放入44。

b. 二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:H_{i} = (H_{0} \pm i^{2}) % m,其中:i = 1,2,3....,H_{0} 是通过散列函数Hash(x)对元素的关键码key进行计算得到的位置,m是表的大小。

平均查找长度(成功/失败)的计算:

成功:sum = (查找成功的总次数)/ 数据集合总个数个数

失败:sum = (查找失败的总次数)/ 数据集合总个数个数

3.2.2 不封闭区间中解决(开散列)——哈系桶(链地址法)

        开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

还是上面的例子:

数据集合{1,7,6,4,5,9};

        开散列中每个桶中放的都是发生哈希冲突的元素。开散列,可以认为是把一个在大集合中的搜索问题转化为在小集合中做搜索了。

4. 冲突严重时的解决办法

        哈希桶其实可以看作将大集合的搜索问题转化为小集合的搜索问题了,那如果冲突严重,就意味着小集合的搜索性能其实也时不佳的,这个时候我们就可以将这个所谓的小集合搜索问题继续进行转化,例如:

  • 每个桶的背后是另一个哈希表
  • 每个桶的背后是一棵搜索树

5. 实现

        哈希表的实现——除留余数法。

//元素类型:long类型
//元素取值范围:>= 0
//哈希函数:除留余数法:key % array.length
public class HashTableV1 {static class ListNode{long key;ListNode next;ListNode(long key){this.key = key;}}private ListNode[] array;private int size;public HashTableV1(){array = new ListNode[8];size = 0;}private int hashCode(long key){return (int)key % array.length;}public boolean add(long key){int index = hashCode(key);for (ListNode cur = array[index]; cur != null; cur = cur.next){if(cur.key == key){//重复return false;}}ListNode node = new ListNode(key);node.next = array[index];array[index] = node;size++;//由于负载因子过大,需要扩容if(1.0 * size / array.length > 0.75){grow();}return true;}private void grow(){ListNode[] newArray = new ListNode[this.array.length * 2];for (int i = 0; i < array.length; i++) {ListNode cur = array[i];while (cur != null){int index = (int) cur.key % newArray.length;ListNode next = cur.next;cur.next = newArray[index];newArray[index] = cur;cur = next;}}this.array = newArray;}public boolean remove(long key){int index = hashCode(key);if(array[index] == null){return false;}//头删if(array[index].key == key){array[index] = array[index].next;size--;return true;}ListNode prev = array[index];ListNode cur = array[index].next;while (cur != null){if(cur.key == key){prev.next = cur.next;size--;return true;}prev = cur;cur = cur.next;}return false;}//平均时间复杂度:O(1)public boolean contains(long key){//1. 把key转成下标——求哈希值的过程int index = hashCode(key);//2. 要查找的链表的头结点就是// 此时index不会越界ListNode head = array[index];for (ListNode cur = head; cur != null ; cur = cur.next) {if(cur.key == key){return true;}}return false;}
}

6. 性能分析

        虽然哈希表一直在和冲突做斗争,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的,也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是O(1)。

7. 和java类集的关系

  • HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set。
  • java 中使用的是哈希桶方式解决冲突的。
  • java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)。
  • java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写hashCode 和 equals 方法,而且要做到 equals 相等的对象,hashCode 一定是一致的。

如有建议或想法,欢迎一起讨论学习~


http://www.ppmy.cn/news/59366.html

相关文章

女朋友说总是记不住Git命令,怎么办?安排!

如果你也和我女朋友一样总是忘记Git命令&#xff0c;觉得记忆Git命令是很枯燥和麻烦的事情。我写了一个包含了40 条常用Git命令的清单。你一定要收藏起来&#xff0c;当你忘记Git命令的时候&#xff0c;就可以打开来查看啦&#xff01;&#xff01;&#xff01; 1.初始化本地仓…

踩坑集锦之hashcode计算

踩坑集锦之hashcode计算 问题对象hashcode怎么计算出来的HotSpot虚拟机是如何计算出对象hashcode的如何根据对象内存地址计算出对象的hashcode 可变对象加哈希缓存导致的错误问题如何解决 hashcode的取值范围& 0x7FFFFFFF 这个操作有什么作用实例演示 最终解决方案 问题 需…

低代码平台组件间事件交互

事件的分类 我们主要依托于事件来进行组件间的交互。为了满足组件与组件、组件与系统、组件与服务端的交互&#xff0c;我们大致可以将事件分为三个类别&#xff1a; 组件方法&#xff1a;每个组件都会暴露出一些方法供其他组件进行调用。例如表格组件&#xff0c;我们可以暴…

第三十章 金马弹灵

巴哥奔此时才觉察到&#xff0c;在这一横一竖两个椭圆交叉而成的怪厅内&#xff0c;五六位身着白裙的小孩正围着一个蓝色滴形水池绕圈圈。水池上方倒挂着两根相互缠绕着的石笋&#xff0c;刚才那滴水正是从两笋之间的弧形嘴中落下的。 水池的高度与他们的头顶平齐&#xff0c;水…

【PowerDesigner】一款超好用的E-R图工具,快速构建出高质量的数据库结构,提高开发效率和代码质量

博主简介&#xff1a;努力学习的大一在校计算机专业学生&#xff0c;热爱学习和创作。目前在学习和分享&#xff1a;数据结构、Go&#xff0c;Java等相关知识。博主主页&#xff1a; 是瑶瑶子啦所属专栏: Mysql从入门到精通 近期目标&#xff1a;写好专栏的每一篇文章 文章目录…

YOLOv5 训练自己的数据集

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客 &#x1f356; 原作者&#xff1a;K同学啊|接辅导、项目定制 ● 难度&#xff1a;夯实基础⭐⭐ ● 语言&#xff1a;Python3、Pytorch3 ● 时间&#xff1a;5月1日-5月6日 &#x1f37a;要求&#xff1…

C语言atoi函数详解

一、atoi&#xff08;&#xff09;基本概念 atoi是C/C语言中一个常用的字符串转整数的函数&#xff0c;其原型定义在stdlib.h头文件中。它的作用是将一个字符串表示的数字转换为对应的整数。 函数原型&#xff1a; int atoi(const char* str); 参数&#xff1a; str&#x…

Vue电商项目--vuex模块开发

vuex状态管理库 vuex是什么&#xff1f; vuex是官方提供的一个插件&#xff0c;状态管理库&#xff0c;集中式管理项目中组件共有的数据。 切记&#xff0c;并不是全部的项目都需要Vuex,如果项目很小&#xff0c;完全不需要vuex,如果项目很大&#xff0c;组件很多&#xff0…