数据结构
程序 = 数据结构 + 算法
数据结构:是相互之间存在的一种或多种特定关系的数据元素的集合。
1.线性表
线性表(List):零个或多个数据元素的有限序列。
public interface List<T> {
public void add(T t);
public void insert(T t, int index);
public T remove(int index);
public boolean isEmpty();
public T get(int index);
public void validate(int index);
public int size();
}
1.顺序线性表
线性表的顺序存储结构:指的是用一段地址连续的存储单元依次存储线性表的数据元素。
线性表(a1,a2…an)的顺序存储示意图如下所示:
/**
* 顺序线性表
* 底层由数组实现,存取速度快(通过下标直接获取),插入删除慢(需要扩容缩容)
* 线性表的存取操作时间性能为 O(1)。我们通常将存取操作具备常数性能(O(1))的存储结构称为 随机存储结构。
* @param <T>
*/
public class ArrayList<T> implements List<T> {
public Object[] objects = {};
private int size;
public ArrayList() {
}
public ArrayList(int len) {
objects = new Object[len];
}
@Override
public void add(T t) {
size++;
objects = Arrays.copyOf(objects, size);
objects[size - 1] = t;
}
public void insert(T t, int index) {
if (index < 0 || index > size) {
throw new RuntimeException();
}
size++;
objects = Arrays.copyOf(objects, size + 1);
System.arraycopy(objects, index, objects, index + 1,
size - index - 1);
objects[index] = t;
}
public T remove(int index) {
validate(index);
T t = (T) objects[index];
System.arraycopy(objects, index + 1, objects, index,
size - index - 1);
size--;
objects = Arrays.copyOf(objects, size);
return t;
}
public boolean isEmpty() {
return size == 0;
}
public T get(int index) {
validate(index);
return (T) objects[index];
}
public void validate(int index) {
if (index < 0 || index > size - 1) {
throw new RuntimeException();
}
}
public int size() {
return size;
}
}
2.链表
链表:一个或多个 结点 组合而成的数据结构称为 链表。其中,结点 一般由两部分内容构成:
- 数据域:存储真实数据元素。
- 指针域:存储下一个结点的地址(指针)。
一般把链表中的第一个结点称为 头指针。有时,为了能更加方便地对链表进行操作,会在单链表的第一个结点(即头指针)前附设一个结点,称为 头结点。头结点的数据域可以不存储任何信息,其指针域存储指向第一个结点的指针(即指向头指针)。如下图所示:
1.单链表
/**
* 单链表存储结构
* @param <E>
*/
@Getter
public class SingleNode<E> {
private E item;
private SingleNode<E> next;
public SingleNode(E element) {
this.item = element;
}
public SingleNode(SingleNode<E> next, E element) {
this.item = element;
this.next = next;
}
public static <E> void addData(E element, SingleNode head) {
//初始化要加入的节点
SingleNode newNode = new SingleNode(element);
//临时节点
SingleNode temp = head;
// 找到尾节点
while (temp.next != null) {
temp = temp.next;
}
// 已经包括了头节点.next为null的情况了~
temp.next = newNode;
}
}
2.双向链表
双向链表(double linked list):在单链表的每个结点中,再设置一个指向其前驱结点的指针域。
/**
* 双向链表
* @param <E>
*/
public class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(E element) {
this.item = element;
}
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.prev = prev;
this.next = next;
}
}
/**
* 双向链表
* @param <T>
*/
public class LinkedList<T> implements List<T> {
transient Node<T> first;
transient Node<T> last;
private int size;
@Override
public void add(T t) {
final Node<T> l = last;
final Node<T> temp = new Node<>(l, t, null);
last = temp;
if (l == null) {
first = temp;
} else {
l.next = temp;
}
size++;
}
@Override
public void insert(T t, int index) {
// 获取index的节点
Node<T> temp = getNode(index);
// 创建新节点
Node nodeNew = new Node(temp.prev, t, temp);
temp.prev.next = nodeNew;
temp.prev = nodeNew;
size++;
}
@Override
public T remove(int index) {
// 获取index的节点
Node<T> temp = getNode(index);
temp.prev.next = temp.next;
temp.next.prev = temp.prev;
size--;
return temp.item;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public T get(int index) {
Node<T> temp = getNode(index);
return temp.item;
}
private Node<T> getNode(int index) {
Node<T> temp;
validate(index);
if (index > size / 2) {
temp = last;
for (int i = size - 1; i > index; i--) {
temp = temp.prev;
}
} else {
temp = first;
for (int i = 1; i <= index; i++) {
temp = temp.next;
}
}
return temp;
}
@Override
public void validate(int index) {
if (index < 0 || index > size - 1) {
throw new RuntimeException();
}
}
@Override
public int size() {
return size;
}
public T getFirst() {
final Node<T> f = first;
if (f == null) {
throw new RuntimeException();
}
return f.item;
}
public T getLast() {
final Node<T> f = last;
if (f == null) {
throw new RuntimeException();
}
return f.item;
}
}
3.循环链表
将单链表中的终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称 循环链表(circular linked list)。
为了使空链表与非空链表处理一致,我们通常设一个头结点(循环链表不一定需要头结点)。其实循环链表和单链表的主要差异就在于循环的判断条件上,单链表判断结束的条件为尾结点是否指向空:p->next = NULL
,而循环链表判定的条件为当前结点是否指向头结点:p->next = head
,是则当前结点为尾结点。
4.线性表与链表的区别
在线性表的顺序存储结构(即数组)中
- 其任意一个元素的存储位置可以通过计算得到,通过下标获取元素,因此其数据读取的时间复杂度为O(1);
- 对任意一个元素的增删操作,其时间复杂度为O(n) ,因为存在移动操作,需要扩容缩容
而对单链表结构来说
-
假设需要获取第 i 个元素,则必须从第一个结点开始依次进行遍历,直到达到第 i 个结点。因此,其数据元素读取的时间复杂度为O(n)。
-
对其任意一个位置进行增删操作,因为需要先进行遍历找到目标元素,其时间复杂度为O(n),
因此,如果只对一个元素进行增删操作,两种结构并不存在优劣之分,但如果针对多个数据进行增删,由于线性表每一次增删都需要移动 n-i 个元素,即每个元素的操作都为O(n);而单链表只在第一次遍历定位目标元素时为 O(n),对后续元素的增删只需简单地赋值移动指针即可,其时间复杂度为O(1)。因此,对于插入或删除数据越频繁的操作,单链表的效率就越明显。
3.栈
栈(stack):是限定仅在表尾(栈顶)进行插入和删除操作的线性表。栈又称为先进后出的线性表
我们把允许插入和删除的一端称为 栈顶(top),另一端称为 栈底(bottom),不含任何任何数据元素的栈称为 空栈。
栈 是线性表的特例,其具备先进后出特性。可以使用线性表的顺序存储结构(即数组)实现栈,将之称之为 顺序栈;可以使用单链表结构实现栈,将之称之为 链栈。两者示意图如下所示:
顺序栈和链栈的时间复杂度均为O(1)。对于空间性能,顺序栈需要事先确定一个固定的长度(数组长度),可能存在内存空间浪费问题,但它的优势是存取时定位很方便;而链栈则要求每个元素都要配套一个指向下个结点的指针域,增大了内存开销,但好处是栈的长度无限。因此,如果栈的使用过程中元素变化不可预料,有时很小,有时很大,那么最好使用链栈;反之,如果它的变化在可控范围内,则建议使用顺序栈。
栈的内部实现原理其实就是数组或链表的操作,而之所以引入 栈 这个概念,是为了将程序设计问题模型化,用高层的模块指导特定行为(栈的先进后出特性),划分了不同关注层次,使得思考范围缩小,更加聚焦于我们致力解决的问题核心,简化了程序设计的问题。
栈 有一个很重要的应用:递归。每个递归定义必须至少有一个条件,使得当满足条件时,递归不再进行。
递归 的一个经典例子为:斐波那契数列(Fibonacci),指的是这样一个数列:1、1、2、3、5、8、13、21、……,即当前位置的值为前面两项之和。第n个数的值为多少
// 递归
int fbi(int n){
if (n > 2){
return fbi(n-1) + fbi(n-2);
}
return 1;
}
// for循环 时间复杂度优于递归
int fbi(int n){
int f1=1;
int f2=f1;
int t=f1;
for(int i=1;i<=n;i++){
if (n > 2){
t=f1+f2;
f1=f2;
f2=t;
}
}
return t;
}
public class Stack<T> implements List<T> {
private Node<T> top;
private Node<T> bottom;
private int size;
public Stack() {
}
public Stack(Node top, Node bottom) {
this.top = top;
this.bottom = bottom;
}
@Override
public void add(Object o) {
Node<T> t = top;
Node<T> newNode = new Node(null, o, t);
top = newNode;
if (t == null) {
bottom = newNode;
} else {
t.prev = newNode;
}
size++;
}
@Override
public void insert(T t, int index) {
// 获取index的节点
Node<T> temp = getNode(index);
// 创建新节点
Node nodeNew = new Node(temp.prev, t, temp);
temp.prev.next = nodeNew;
temp.prev = nodeNew;
size++;
}
@Override
public T remove(int index) {
// 获取index的节点
Node<T> temp = getNode(index);
temp.prev.next = temp.next;
temp.next.prev = temp.prev;
size--;
return temp.item;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public T get(int index) {
Node<T> node = getNode(index);
return node.item;
}
@Override
public void validate(int index) {
if (index < 0 || index > size - 1) {
throw new RuntimeException();
}
}
@Override
public int size() {
return size;
}
public T getTop() {
final Node<T> f = top;
if (f == null) {
throw new RuntimeException();
}
return f.item;
}
public T getBottom() {
final Node<T> f = bottom;
if (f == null) {
throw new RuntimeException();
}
return f.item;
}
private Node<T> getNode(int index) {
Node<T> temp;
validate(index);
if (index > size / 2) {
temp = top;
for (int i = size - 1; i > index; i--) {
temp = temp.next;
}
} else {
temp = bottom;
for (int i = 1; i <= index; i++) {
temp = temp.prev;
}
}
return temp;
}
}
队列(queue):是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列 是一种 先进先出(First In First Out) 的线性表。允许插入的一端称为队尾,允许删除的一端称为对头。
线性表有顺序存储和链式存储,栈是线性表,所以有这两种存储方式。同样,队列作为一种特殊的线性表,也同样存在这两种存储方式。
2.树
树(Tree): 树是n(n>=0)个结点的有限集。当n=0时称为空树。
1.二叉树
二叉树(Binary Tree):是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的,分别称为根结点的左子树和右子树的二叉树组成。如下图所示:
二叉树的特点
- 每个结点最多只能有两棵子树。
- 左子树和右子树是有顺序的,次序不能任意颠倒。
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
二叉树的遍历
二叉树的遍历(traversing binary tree):是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
前序遍历:规则是先访问根结点,然后前序遍历左子树,再前序遍历右子树(总结:根结点 -> 左子树 -> 右子树)。如下图所示,遍历的顺序为:ABDGHCEIF。
中序遍历:从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后再访问根结点,最后中序遍历右子树(总结:左子树 -> 根结点 -> 右子树)。如下图所示,遍历的顺序为:GDHBAEICF。
后序遍历:从左到右先叶子后结点的方式遍历访问左右子树,最后访问根结点(总结:从左到右访问叶子结点 -> 根结点)。如下图所示,遍历的顺序为:GHDBIEFCA。
层序遍历:从树的第一层,即根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问(总结:第一层 -> 第二层(从左到右访问结点)-> ··· -> 最后一层(从左到右访问结点)。如下图所示,遍历的顺序为:ABCDEFGHI。
/**
* 二叉树
*
* 中序遍历
* 先访问根节点,然后访问左节点,最后访问右节点(根->左->右)
* 先序遍历
* 先访问左节点,然后访问根节点,最后访问右节点(左->根->右)
* 后序遍历
* 先访问左节点,然后访问右节点,最后访问根节点(左->右->根)
*
* @param <E>
*/
@Data
public class TreeNode<E> {
TreeNode<E> leftNode;
E item;
TreeNode<E> rightNode;
public TreeNode() {
}
public TreeNode(E item) {
this.item = item;
}
public TreeNode(TreeNode<E> leftNode, E item, TreeNode<E> rightNode) {
this.leftNode = leftNode;
this.item = item;
this.rightNode = rightNode;
}
/**
* 10
* 9 20
* 15 35
*/
/**
* 如果是中序遍历:10->9->20->15->35
*
* 如果是先序遍历:9->10->15->20->35
* 解释:访问完10节点过后,去找的是20节点,但20下还有子节点,因此先访问的是20的左节点15节点。
* 由于15节点没有子节点了。所以就返回20节点,访问20节点。最后访问35节点
*
* 如果是后序遍历:9->15->35->20->10
* 解释:先访问9节点,随后应该访问的是20节点,但20下还有子节点,因此先访问的是20的左节点15节点。
* 由于15节点没有子节点了。所以就去访问35节点,由于35节点也没有子节点了,所以返回20节点,最终返回10节点
*
* @param treeNode
*/
public static void inTraverseBTree(TreeNode treeNode) {
if (treeNode != null) {
System.out.println(treeNode);
//访问左节点
inTraverseBTree(treeNode.getLeftNode());
//访问右节点
inTraverseBTree(treeNode.getRightNode());
}
}
@Override
public String toString() {
return item.toString();
}
}
线性表与树的区别
2.二叉查找树
二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树。
上面这张图就是一个二叉查找树。二叉查找树有如下几条特性
- 左子树上所有结点的值均小于或等于它的根结点的值。
- 右子树上所有结点的值均大于或等于它的根结点的值。
- 左、右子树也分别为二叉查找树
比如我们要查找10这个元素,查找过程为:首先找到根节点,然后根据二叉查找树的第一二条特性,我们知道要查找的10>9所以是在根节点的右边去查找,找到13,10<13,所以接着在13的左边找,找到11,10<11,继续在11的左边查找,这样就找到了10.这其实就是二分查找的思想。最后我们要查出结果所需的最大次数就是二叉树的高度!
那既然要查出结果所需的最大次数就是二叉树的高度,那这个高度会不会有时候很长呢?
比如我们依次插入 根节点为9如下五个节点:7,6,5,4,3。依照二叉查找树的特性,结果会变成什么样呢?7,6,5,4,3一个比一个小,那么就会成一条直线,也就是成为了线性的查询,时间复杂度变成了O(N)级别。为了解决这种情况,该红黑树出场了。
3.平衡二叉树
平衡二叉树(Balanced BinaryTree)又被称为AVL树。在二叉查找树的基础上,平衡二叉树上任何节点的左、右子树的深度之差都不会超过1
4.红黑树
1.红黑树
红黑树其实就是一种自平衡的二叉查找树。他这个自平衡的特性就是对HashMap中链表可能会很长做出的优化。
红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
- 节点是红色或黑色。
- 根节点是黑色。
- 每个叶节点(NIL节点,空节点)是黑色的。
- 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
下面这棵树就是一个典型的红黑树
红黑树那么多规则,那么在插入和删除元素会不会破坏红黑树的规则呢?什么情况下会破坏?什么情况下不会?
比如我们向原红黑树插入为14的新节点就不会破坏规则
向原红黑树插入值为21的新节点就破坏了规则
那么红黑树是怎么维护这个二叉查找树的自平衡性的呢?
红黑树通过**“变色”和“旋转”**来维护红黑树的规则,变色就是让黑的变成红的,红的变成黑的,旋转又分为“左旋转”和“右旋转”
2.hashmap1.7
数组+链表
默认HashMap内部数组的长度为16,负载因子为0.75,就是在构造函数里面传的两个值。阈值就是12(16*0.75=12),这样当第十三个元素加入时,底层数组就会扩容。扩容为原数组大小的两倍。
存储的时候就是拿到key的hash值然后对HashMap底层数组的长度取余,取余的结果就是存储的索引。
取值的时候一样是拿到key的hash值然后对HashMap底层数组的长度取余,得到索引直接去数组里面取就行了。取出来是一个链表的封装类Entry,然后遍历一下Entry中的值取出我们要的就行了。
HashMap我们知道是数组+链表实现的,前面我们是只看到了数组,链表呢就是在这使用的。这里HashMap里面用到链式数据结构的一个概念。上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。也就是说数组中存储的是最后插入的元素。(数组中只会存一个Entry元素,这一个元素的next就会指向下一个元素,这样循环)
3.hashmap1.8
数组+链表+红黑树
解决链表查询过长效率过低的问题
HashMap中的红黑树节点 采用的是TreeNode 类。TreeNode和Entry都是Node的子类,也就说Node可能是链表结构,也可能是红黑树结构。
如果插入的key的hashcode相同,那么这些key也会被定位到Node数组的同一个格子里。如果同一个格子里的key不超过8个,使用链表结构存储。如果超过了8个,那么会调用treeifyBin函数,将链表转换为红黑树
4.简易hashmap实现
public interface Map<K,V>{
V put(K k,V v);
V get(K k);
int size();
interface Entry<K,V>{
K getKey();
V getValue();
}
}
public class HashMap<K,V> implements Map<K,V>{
private Entry<K,V> table = null;
int size=0;
public HashMap(){
this.table=new Entry[16];
}
/**
* 通过key hash算法算出数组的index下标
* 根据index找到当前下标对象
* 判断当前对象是否为空,为空则直接存储
* 如果不为空,则next链表
* 返回当前节点对象
*/
@Override
public V put(K k,V v){
//hashcode值可能为负
int hash = k.hashCode()>=0? k.hashCode():-k.hashCode();
int index = hash%16;
Entry<K,V> entry = table[index];
if(null == entry){
//todo hash还是index
table[index] = new Entry<>(k,v,hash,null);
size++;
}else{
table[index] = new Entry<>(k,v,hash,entry);
}
return table[index].getValue();
}
/**
* 通过key hash算法算出数组的index下标
* 根据index找到当前下标对象
* 判断当前对象是否为空,不为空则判断是否相等
* 不相等,判断next是否相等,一直判断next、直到相等为止或者为空
*
*/
@Override
public V get(K k){
if(size == 0){
return null;
}
int index = hash(k);
Entry<K,V> entry = table[index];
return findValue(entry,k);
}
@Override
public int size(){
return size;
}
/**
* 通过key hash算法算出数组的index下标
*/
private int hash(K k){
int index = k.hashCode() %16;
//hashcode值可能为负
return index>=0?index:-index;
}
/**
* 递归、查找值
*/
private Entry<K,V> findValue(Entry<K,V>,K k){
if(entry==null){
return null;
}
if(k.equals(entry.getKey()) || k==entry.getKey()){
return entry;
}else{
if(entry.next!=null){
return findValue(entry.next, k);
}
}
return null;
}
class Entry<K,V> implements Map.Entry<K,V>{
K k;
V v;
//hashCode值
int hash;
//链表中下一个对象
Entry<K,V> next;
public Entry<K,V> (K k,V v,int hash,Entry<K,V> next) {
this.k=k;
this.v=v;
this.hash=hash;
this.next=next;
}
@Override
public K getKey(){
return k;
}
@Override
public V getValue(){
}
}
}
5.B树与B+树
B树
多路平衡查找树。每个节点都存储key和data,所有节点组成这棵树
- 叶节点具有相同的深度
- 叶节点的指针为空 所有索引元素不重复
- 节点中的数据索引从左到右递增排序
B+树
B树的变种,只有叶子节点存储data,叶子节点包含了这棵树的所有键值
- 非叶子节点不存储data,只存储索引(冗余),可以放更多的索引
- 叶子节点包含所有索引字段
- 叶子节点用双向指针连接,提高区间访问的性能
一般来说,索引很大,往往以索引文件的形式存储的磁盘上,索引查找时产生磁盘I/O消耗,磁盘一次IO大概4k左右数据,最多几十k,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的时间复杂度。树高度越小,I/O次数越少。
那为什么是B+树而不是B树呢,因为它内节点不存储data,这样一个节点就可以存储更多的key。
在MySQL中,最常用的两个存储引擎是MyISAM和InnoDB,它们对索引的实现方式是不同的。
MyISAM
data存的是数据地址。索引是索引,数据是数据。索引放在XX.MYI文件中,数据放在XX.MYD文件中,所以也叫非聚集索引。
InnoDB
MySQL一次能加载的数据
data存的是数据本身。索引也是数据。数据和索引存在一个XX.IDB文件中,所以也叫聚集索引。
了解了数据结构再看索引,一切都不费解了,只是顺着逻辑推而已。另加两种存储引擎的区别:
1、MyISAM是非事务安全的,而InnoDB是事务安全的
2、MyISAM锁的粒度是表级的,而InnoDB支持行级锁
3、MyISAM支持全文类型索引,而InnoDB不支持全文索引
4、MyISAM相对简单,效率上要优于InnoDB,小型应用可以考虑使用MyISAM
5、MyISAM表保存成文件形式,跨平台使用更加方便
6、MyISAM管理非事务表,提供高速存储和检索以及全文搜索能力,如果在应用中执行大量select操作可选择
7、InnoDB用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,可选择。
3.测试
public class Test1 {
public static void main(String[] args) {
// test(new ArrayList());
// test(new LinkedList());
// test(new Stack());
testTreeNode();
}
public static void testSingleNode() {
SingleNode<String> node = new SingleNode<String>("1");
for (int i = 2; i < 6; i++) {
SingleNode.addData(i + "", node);
}
SingleNode temp = node;
while (temp.getNext() != null) {
System.out.println(temp.getItem());
temp = temp.getNext();
}
}
public static void test(List list) {
System.out.println(list.size());
System.out.println(list.isEmpty());
for (int i = 1; i < 6; i++) {
list.add(i + "");
}
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + ",");
}
System.out.println();
list.insert("7", 2);
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + ",");
}
System.out.println();
list.remove(4);
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + ",");
}
System.out.println();
System.out.println(list.size());
System.out.println(list.isEmpty());
if (list instanceof LinkedList) {
System.out.println(((LinkedList) list).getFirst());
System.out.println(((LinkedList) list).getLast());
}
if (list instanceof Stack) {
System.out.println(((Stack) list).getTop());
System.out.println(((Stack) list).getBottom());
}
System.out.println();
}
/**
* 10
* 9 20
* 15 35
*/
public static void testTreeNode() {
//根节点-->10
TreeNode treeNode1 = new TreeNode(10);
//左-->9
TreeNode TreeNode = new TreeNode(9);
//右-->20
TreeNode treeNode3 = new TreeNode(20);
//20的左-->15
TreeNode treeNode4 = new TreeNode(15);
//20的右-->35
TreeNode treeNode5 = new TreeNode(35);
treeNode1.setLeftNode(TreeNode);
treeNode1.setRightNode(treeNode3);
treeNode3.setLeftNode(treeNode4);
treeNode3.setRightNode(treeNode5);
TreeNode.inTraverseBTree(treeNode1);
}
public static void testHashMap() {
Map<String,String> map = new HashMap<>();
map.put("刘一","刘一");
map.put("陈二","陈二");
map.put("张三","张三");
map.put("李四","李四");
map.put("王五","王五");
System.out.println(map.get("张三"));
}
}