集合框架
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的树
区分左子树和右子树的顺序
性质
-
在深度为k的二叉树中,最多存在2^k - 1个节点
-
在第k层,最多存在2^(k - 1)个节点
-
度为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()方法扩容/初始化。