目录
一.搜索树
我们如果想学习一下Set&Map的话,不妨先学习一下其本质——搜索树!
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
1.若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
2.若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
3.它的左右子树也分别为二叉搜索树
模拟实现:
1.插入节点
①. 先 root == null 直接让root 等于该节点
②. 然后找到叶子节点
③. 但是插入的时候,得有parent记录cur 的前一个父亲节点 然后看parent与插入节点的关系。
public boolean insert(int key){
TreeNode cur = new TreeNode(key);
if(root == null){
root = cur;
}
TreeNode parent = null;
cur = root;
while(cur != null){
if(cur.val > key){
parent = cur;
cur = cur.left;
}else if(cur.val < key){
parent = cur;
cur = cur.right;
}else{
return false;
}
}
TreeNode tar = new TreeNode(key);
//此时cur == null了
if(parent.val < key){
parent.right = tar;
}else{
parent.left = tar;
}
return true;
}
2.搜索节点
利用搜索树的性质进行搜索!
public TreeNode search(int val){
TreeNode cur = root;
while(cur != null){
if(cur.val < val){
cur = cur.right;
}else if(cur.val > val){
cur = cur.left;
}else{
return cur;
}
}
return null;
}
3. 删节点 ?
public void remove(int key) {
TreeNode parent = null;
TreeNode cur = root;
while (cur != null) {
if(cur.val == key) {
removeNode(parent,cur);
return;
}else if(cur.val < key) {
parent = cur;
cur = cur.right;
}else {
parent = cur;
cur = cur.left;
}
}
}
private void removeNode(TreeNode parent,TreeNode cur){
if(cur.left == null){
if(cur == root){
root = root.right;
}else if(parent.left == cur){
parent.left = cur.right;
}else{
parent.right = cur.right;
}
} else if (cur.right == null) {
if(cur == root){
root = cur.left;
}else if(parent.left == cur) {
parent.left = cur.left;
}else{
parent.right = cur.left;
}
}else {
TreeNode target = cur.right;
TreeNode targetParent = cur;
while(target.left != null){
targetParent = target;
target = target.left;
}
//这里已经找到了右树的最左节点
cur.val = target.val;
//删节点
//找最小值的时候,left没有数据
if(targetParent.right == target){
targetParent.right = target.right;
}else{
targetParent.left = target.right;
}
}
}
我们要知道的是:TreeMap和TreeSet本质都是一颗搜索树,但这棵搜索树比较特殊 是红黑树!
二.Map:
Map是一个接口类,该类没有继承自Collection,该类中存储的是<K,V>结构的键值对,并且K一定是唯一的,不能重复
1.1 map的模型
map存储的是Key-Value键值对
注意TreeMap实现了SortedMap接口:TreeMap可以比较的!
意味着 存入的数据key是可比较的(否则 就会报错 !),而且存入的时候是比较之后再存入的!
所以,如果是put的时候key是一个自定义类:就需要实现comparable接口,重写其方法 亦或者传一个比较器!
1.2 map方法:
要注意的是 entrySet()方法:
可以理解为 :将Map内的数据key与value组织变成一个整体(此结构叫Map.Entry<K,V>),起来放入一个Set的大麻袋里(无规则放入)
//此方法为了遍历集合! 且map没有实现Iterable接口
1. Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap
2. Map中存放键值对的Key是唯一的,value是可以重复的
3. Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复)。
4. Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。
5. Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入
三.关于Map.Entry<K, V>的说明:
Map.Entry<K, V> 是Map内部实现的用来存放<key, value>键值对映射关系的内部类,该内部类中主要提供了<key, value>的获取,value的设置以及Key的比较方式。
因此entry之后再使用以上三种方法!
四.Set:
1.1 set
与Map不同的是:Set继承Collection的接口类,Set只存储了Key!
不能存储相同的元素
且key必须是可比较的,但是为啥这样呢?
观察add源码可以知道 相当于其本质就是TreeMap,但是value值变成固定的PRESENT!
TreeMap与TreeSet的当中去存储元素的时候,他们的key一定可以比较,否则会出异常
2.2 Set 方法
1. Set是继承自Collection的一个接口类
2. Set中只存储了key,并且要求key一定要唯一
3. Set的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入Map中的
4. Set最大的功能就是对集合中的元素进行去重
5. 实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础上维护了一个双向链表来记录元素的插入次序。
6. Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入
7. Set中不能插入null的key。
五. 哈希表:
这种数据结构可以实现搜索时达到O(1)的时间复杂度!
5.1 概念
可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函
数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
该方式叫哈希方法/散列方法
转换函数叫哈希函数
这种数据结构叫哈希表
5.2 哈希冲突
对于两个数据元素的关键字 和 (i != j),有 != ,但有:Hash( ) == Hash( ),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
5.2.1 哈希冲突-避免
我们要知道哈希冲突是避免不了的,只能降低其发生的概率
①哈希函数设置不合理
1. 直接定制法--(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关
键字的分布情况 使用场景:适合查找比较小且连续的情况 面试题:字符串中第一个只出现一次字符
2. 除留余数法--(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:
Hash(key) = key% p(p<=m),将关键码转换成哈希地址
3. 平方取中法--(了解)
比特就业课
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对
它平方就是18671041,抽取中间的3位671(或710)作为哈希地址 平方取中法比较适合:不知道关键字的分
布,而位数又不是很大的情况
4. 折叠法--(了解)
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,
并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
5. 随机数法--(了解)
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数
函数。
通常应用于关键字长度不等时采用此法
6. 数学分析法--(了解)
设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某
些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据
散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。例如:
假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是 相同的,那么我们可以
选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还可以对抽取出来的数字进行反转(如
1234改成4321)、右环位移(如1234改成4123)、左环移位、前两数与后两数叠加(如1234改成12+34=46)等方
法。
②负载因子
所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。
已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小。
5.2.2 哈希冲突的解决
1.闭散列:
①.线性探测
当一个元素进行存入的时候进行完哈希函数之后那个位置有节点,就找下一个空的位置。
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
②.二次探测
2.开散列:
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
可以理解为 数组 + 链表 的结构
而且当数组长度超过64并且链表的长度超过8的时候,就会变成红黑树!
与java中类集的关系:
1. HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set
2. java 中使用的是哈希桶方式解决冲突的
3. java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
4. java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方法,而且要做到 equals 相等的对象,hashCode 一定是一致的。(非常重要!!!)
模拟实现HashMap
public class HashBucket {
private static class Node {
private int key;
private int value;
Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
private Node[] array;
private int size; // 当前的数据个数
private static final double LOAD_FACTOR = 0.75;
private static final int DEFAULT_SIZE = 8;//默认桶的大小
public void put(int key, int value) {
// write code here
Node node = new Node(key,value);
int index = key/array.length;
Node cur = array[index];
while(cur != null){
if(cur.key == key){
cur.value = value;
return;
}
cur = cur.next;
}
//重复的已经更新后value值
node.next = array[index];
array[index] = node;
size++;
if((size * 1.0f / array.length) >= 0.75f){
resize();
}
}
private void resize() {
// write code here
Node[] newArray = new Node[DEFAULT_SIZE * 2];
for(int i = 0;i < array.length;i++){
Node cur = array[i];
while(cur != null){
int newIndex = size / newArray.length;
cur.next = newArray[newIndex];
newArray[newIndex] = cur;
cur = cur.next;
}
}
array = newArray;
}
private double loadFactor() {
return size * 1.0 / array.length;
}
public HashBucket() {
// write code here
array = new Node[DEFAULT_SIZE];
}
public int get(int key) {
// write code here
int index = key % array.length;
Node head = array[index];
while (head != null) {
if(head.key == key) {
return head.value;
}
head = head.next;
}
return -1;
}
}