DS_01_栈、队列、二叉树、堆、Map和Set

集合框架

java.util包下

在这里插入图片描述

List

线性表:数组、字符串(内部是字符数组)、链表、栈、队列。

线性表一次保存单个同类型元素

List是线性表的父接口

常用子类:

  • ArrayList(动态数组实现)
  • LinkedList(双性链表实现)
//List接口中的方法使用一下
add(E e);//尾插
add(int index, E e);

remove(E e);//删第一个值为e
remove(int index);

set(int index, E e);

get(int index);
boolean contains(E e);
List<List<Integer>>  //保存整型的二维数组
栈和队列

只能从一端插入元素、从同一端取出元素。先进后出、后进先出(LIFO,Last in first out)

核心操作

push();

pop();

peek();

顺序栈

基于数组实现的栈

链式栈

基于链表实现的栈

双栈思路

队列

FIFO,先进先出,元素从“队尾”添加到队列中,从“队首”出队列。元素的出队顺序和入队顺序一致

栈和队列都是一码事,都只能从线性表的一端进行插入和删除。因此,栈和队列可以相互转换。

顺序队列
链式队列

采用链表结构实现队列更好

核心操作

offer 入队
poll 出队
peek
isEmpty()

Java.util.Queue

循环队列 LoopQueue

使用长度固定的数组实现,数组头部就是队首,用head引用表示;数组尾部就是队尾,用引用tail表示。[head,tail)是循环队列的有效元素。

删除:head后移;添加:tail后移;走到数组末尾再从头开始走。逻辑删除

在删除元素时,不需要进行元素的搬移。

当新元素添加时,该处元素被覆盖,即做到真正的物理删除

tail+1 == head -->已满

双端队列

Queue的子接口Deque

既可以尾插头出

也可以头插尾出

无论使用栈还是队列统一使用双端队列接口,实例化LinkedList<>().入栈:push(),出栈:pop(),入队:offer(),出队:poll()

不推荐使用Stack,效率很低。

树结构具有高效的查找与搜索语义

概念

树是一种非线性的数据结构,由N个有限节点组成一个具有层次关系的集合。

  • 有一个根节点:没有前驱节点
  • 每一棵树(包括子树),除其根节点外,其余节点被分为m个互不相交的集合
  • 子树不相交
  • 除了根节点外,每个节点有且仅有一个父节点
  • 一颗N个节点的树有N-1条边
  • 节点的度:该节点中包含的子树的个数,树中最大的节点的度是树的度
  • 叶子节点:度为0 的节点
  • 非叶子节点:度不为0的点
  • 根节点:没有父节点的节点,在树中有且仅有一个
  • 节点的层次:从根节点开始计算,根节点在第一层
  • 树的高度:当前树中节点层次最大值
  • 兄弟节点
  • 堂兄弟节点:双亲在同一层的节点互称堂兄弟

二叉树

二叉树是:节点的度最大为2的树

区分左子树和右子树的顺序

性质
  1. 在深度为k的二叉树中,最多存在2^k - 1个节点

  2. 在第k层,最多存在2^(k - 1)个节点

  3. 度为2的节点个数:n2; 度为0的节点个数:n0,度为1的节点个数:n1

    n0 = n2 + 1

    解释:从节点总和来看,n = n0 + n1 + n2

    从边的数量来看,n1 + 2 * n2 = n - 1

满二叉树

每一层节点的个数都达到了最大值

完全二叉树

定义:完全二叉树的节点编号序列和满二叉树的编号序列完全一致

完全二叉树中,若存在度为1的节点,那么只可能找到一个这样的节点,这个节点只有左子树没有右子树。

根节点从0开始编号,编号为i的节点的父节点的编号为(i - 1)/2;

如果有左子节点,其编号为2*i+1;

如果有右子节点,其编号为2*i+2;

二分搜索树(BST:Binary Research Tree)

对于每一棵树,左子树所有节点值 < 根节点值 < 所有右子树节点值

在BST中查找元素==》二分查找

平衡二叉树

该树中任意一个节点的左右子树高度差 <= 1

AVL:严格平衡BST

RBTree:"黑节点"严格平衡的BST

B+树

23树

二叉树的遍历

所有二叉树问题的解决思路都是遍历问题的衍生

遍历:按照一定的顺序访问这个集合的所有元素,做到不重复、不遗漏。

深度优先遍历(DFS)

借助栈实现:先序,第一次访问到该节点就可以执行操作;中序,第二次;后序,第三次;每个节点三次访问后就弹栈

前序遍历

先访问根节点,然后递归访问左子树,再递归访问右子树

根->左->右

中序遍历

左->根->右

后序遍历

左->右->根

广度优先遍历(BFS)

借助队列实现

层序遍历

将二叉树从上到下,一层层遍历,每一层从左到右

//构建一棵树
//先序中序后序遍历
//计算总节点数、叶子节点数、第K层的叶子节点、求树深、判断是否包含指定的值val

在力扣或牛客上做题,如果需要用到全局变量,不要加static关键字(其他人的修改可以改变自己的变量值)

判断完全二叉树:第一阶段,在该阶段下,每个节点都有左右子树。当一个树只有右树没有左树,直接返回false.当碰到第一个只有左子树没有右子树的节点就切换状态,进入第二阶段。

第二阶段,每个节点都是叶子结点。若发现该阶段下的节点有子树,直接返回false.

阶段一:全是度为2的节点;

阶段一与阶段二的分界点:度为1的节点(如果存在,那么有且仅有一个,有左树,无右树;可以没有该节点)

阶段二:全是度为0的节点

//958

//思考:树的构建问题index++次数

优先级队列(堆)

按照优先级的大小动态出队(元素个数动态变化,而非固定)

普通队列是:FIFO

在计算机领域,若见到O(logN)时间复杂度,几乎一定和“树”结构有关,算法逻辑上是一棵树。

二叉堆

二叉堆是基于二叉树的堆,堆结构除了二叉堆还有d叉堆、索引堆

特点

二叉堆是一棵完全二叉树,基于数组存储。

大根堆(最大堆)

每一棵子树的根节点的值>=树中所有节点的值

小根堆(最小堆)

每一棵子树的根节点的值<=树中所有节点的值

节点的层次和大小没有直接关系,例如对于大根堆来说,不能认为层次越高,节点值越大,只能比较树根和其子树的内部节点之间的大小关系

JDK中堆默认是小根堆PriorityQueue<>

节点编号

堆基于数组存储,节点编号为数组索引,即:从零开始编号。

一个节点编号为i;

其父节点的编号为:(i - 1) / 2;

其左子节点的编号为:2 * i + 1;

其右子节点的编号为:2 * i + 2;

在数组中可通过索引来找到树节点的对应关系

核心操作
//基于动态数组实现大根堆
siftUp();
siftDown();
//O(n)时间复杂度实现的,给一个数组,构建最大堆

比较器接口

之前写过java.util.Comparable接口

java.util.Comparator接口

一个类实现了这个接口,表示这个类就是为了别的类的比较大小而服务的。

在定义的类内部实现compare()方法

TopK问题

在数组中找出前k个满足一定条件的数,k<<n(数组元素个数);

内部类

把一个类嵌套进入另一个类内部

匿名内部类
Lamda表达式
new PriorityQueue<>((o1,o2)->o1-o2);
//对内部类再进行优化

此类问题都可以用优先级队列解决

若需要取出前k个最大的元素,构造最小堆;取最小的k个元素,构造最大堆。

其时间复杂度为:O(n*logk);

Map和Set

用到Map和Set时,绝大多数是在进行查找

在java标准库中,二分搜索树是TreeMap/TreeSet;哈希表是HashSet/HashMap.

Set:披着Set外衣的Map;

Set、List、Queue都是Collection接口的子接口

Collection接口:一次只能添加一个元素,其子类都可以使用foreach循环(因为Collection是Iterable的子接口)

Set和List最大的不同:Set内的元素不重复,使用Set集合进行去重处理

Set集合的子类实际上在存储元素时放在了Map集合的Key中,所有的key共用一个Object类对象作为value值。

所以Set保存的元素不可重复。

HashSet就是在HashMap中保存的,HashSet可以保存null;

TreeSet就是在TreeMap中保存的,TreeSet不可以保存null;

//Set接口没有set()方法,不能修改Set结合内的元素
//添加成功返回true,添加失败返回false
boolean add(E e);
//删除成功返回true,删除失败返回false
boolean remove(Object o);
boolean contains(Object o);

存储一个键值对数据 key = value,保存映射关系。

Map<K,V> map = new HashMap<>();
//key值不重复,value可以是重复的
map.put(a,b)
V put(K key, V val);//新增/修改
V remove(Object key);//根据key值删除键值对

V get(Object key); //根据key值查询对应的value,若key值不存在,则返回null
getOrDefault(Object key, Object defaultValue);//若集合中没有key值,返回默认的value值

Set<K> keySet();
Collection<V> values();
Map.Entry entrySet();

//遍历键值对

Map接口常见子类添加问题
  • 元素的添加顺序和保存顺序没有关系,不像LIst
  • HashMap是基于哈希表的实现;TreeMap是基于RBTree(红黑树/二分平衡搜索树)的实现
  • HashMap和TreeMap的区别:在TreeMap中,要保存自定义类型到Key,这个类必须实现Comparable接口或传入比较器Comparator,而在HashMap中没有此要求。
  • 在HashMap中,可以保存null作为key值,这个null的key值只能有一个;在TreeMap中key值不能为空,value可以为空。
//138,尝试用纯链表做;在Map帮助下做;
//290,双循环;双映射;
//旧键盘 ACM模式

搜索树

基础二叉树的衍生结构在实际工程中有着广泛的应用

AVL,RBTree,2-3树,B树

BST:也叫做,二分搜索树、二叉搜索树、二叉排序树

  • BST也是一个二叉树
  • 每个子树的左子树的所有节点值 < 根节点值 < 右子树的所有节点值(JDK中的BST,不存在重复节点)
  • 存储节点必须具备可比较的能力(要么实现了Comparable接口、要么传入比较器)
增删改查
//插入 O(n)(单支树) - O(logn)

//查找,递归地和树根比较
private boolean contains(Node root, int val);//查找以root为根的树是否包含val
//打印,按先序遍历

//删除最大值、最小值

哈希表

哈希函数:将任意数据类型转为数组索引的方法,根据数组索引查找。

哈希表:将任意key值经过哈希函数的运算转换为相应的索引值。

若得到的索引值在哈希表中没有元素保存,则直接保存;

若已经存在,则处理哈希冲突后保存。

  • 用空间换时间
  • 哈希表的高效查找秘诀在于数组的随机访问能力。数组中若知道索引,可以在O(1)时间复杂度内获取该元素。
哈希函数

将任意数据类型转为索引

哈希冲突:不同key值经过hash函数的计算得到了相同的值

一般来说,将任意的正整数映射为小区间数字的常用做法:取模,模素数。模多大的数字,就开辟多大空间的整型数组。

int->int;
char->int;
String->int;    //md5
//任意自定义类型 -toString()方法转为String类型,再利用String类型的哈希函数进行运算
哈希函数的设计
  • 哈希冲突在数学领域理论上是一定存在
  • 尽可能在空间和时间上求平衡,利用最少的空间获取一个较为平均的数字分布。

哈希表的重点:哈希函数的设计与哈希冲突的解决

现成的设计方案:

MD5 MD4 MD3 SHA1 SHA256

MD5
  • 一般用在字符串计算hash值
  • 定长,无论输入的数据有多长,得到的MD5值长度固定
  • 分散,如果输入的数据稍有偏差,得到的值,相差很大(这样的特点使其冲突概率非常低,工程领域忽略不计)
  • 不可逆,根据字符串计算MD5很容易,想通过MD5还原字符串非常难,基本不可能实现
  • 根据相同的数据计算的MD5值是稳定的,只要输入数据相同,MD5的值就不会发生变化(所有哈希函数的设计都要遵守)

用途

  • hash运算
  • 用于加密
  • 对比原文件的内容(上传下载)
哈希冲突的解决
闭散列

发生冲突时,找到冲突位置旁边是否存在空闲位置,直到找到第一个空闲位置放入元素。好存难查更难删,工程中很少使用!

如果该位置已经有元素,那么可以从该位置向后一个一个位置看,找到为空的位置存元素;还可以在该位置往后模一个更小的数,二次哈希来解决冲突。

若整个哈希表的冲突十分严重,此时查找一个元素就从O(1) --> O(n)

开散列

若出现哈希冲突,就让这个位置变为链表。第一个存入该位置的元素就是链表头节点,后面每一次在该位置的冲突,就是该处链表的尾插入。遇到冲突位置元素的查找,就是遍历小链表。

哈希表 = 数组 + 链表

链表过长的话:

  • (C++STL方案)对整个数组扩容,扩容后的数组,原来集中冲突在一个索引上的元素就会比较均衡地分布在新数组中。
  • (JDK方案)将冲突严重的链表再次变为新的哈希表或BST ,这样就可以把链表的O(n)的查找变为O(logn)。

基于开散列实现哈希表

负载因子

负载因子loadfactor的定义是:

α = 哈希表中有效元素个数 / 哈希表的长度,数组长度是定值,α与填入哈希表的元素个数成正比。散列表(哈希表)的平均查找长度是负载因子α的函数

对于开放定址法,负载因子应严格限制在0.7-0.8以下。超过此值将对散列表进行resize(扩容)

JDK的HashMap默认负载因子是0.75,当负载因子超过0.75,就给数组扩容

阿里实验得出:在一亿规模数据下,负载因子设为10以内,查询效率都比较高

Java知识点

类集:源码阅读+数据结构

多线程(线程启动方式、线程状态切换、线程同步与互斥、线程安全、线程池的工作流程)

JVM

边角知识点

Object

Object提供的hashCode()可以将任意对象转为int,不同的对象原则上一定转为不同的int类型的值。equals()方法:判断是否是同一个对象

对象a.equals(对象b)返回true,一定推出它们的hashCode()返回值相同;

自定义对象作为key值的唯一性,就是通过equals()方法保证的。

两个对象的hashCode返回值相同,不一定能推出对象a.equals(对象b)返回true.

JDK源码分析

进入源码的方式:双击shift,搜索类名;Ctrl+鼠标左键 Ctrl+Alt+左键:子类实现接口的方法

HashMap
//JDK8之前HashMap的结构:数组+链表
//JDK8之后的HashMap结构:数组+链表+红黑树(冲突严重的链表会被“树化”,将链表转为红黑树,提高冲突严重的链表查询效率):链表元素个数 > 8 && 哈希表中元素超过64,则将链表进行树化;链表元素个数 > 8 && 哈希表元素个数 <= 64,则不会将链表树化,只是将数组resize,进行扩容。
put;
resize();    //JDK1.8,HashMap.java第720行暂未理解

lazyload:无参构造函数时,不初始化数组,当put时,数组为空,通过resize()方法扩容/初始化。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

AbyssPraise

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

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

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

打赏作者

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

抵扣说明:

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

余额充值