就业冲刺之数据结构
数据结构
双向链表:
-
应用:
LinkedList
-
什么是双向链表:
- 双向链表是有若干个阶段构成,其中每个节点均匀3部分构成(前驱阶段,元素本身,后续节点),这些节点在内存中是游离状态的存在.
操作双向链表
- 双向链表是有若干个阶段构成,其中每个节点均匀3部分构成(前驱阶段,元素本身,后续节点),这些节点在内存中是游离状态的存在.
-
增删元素:(默认在尾部添加)将链断开,然后重新链接,从而实现添加/删除元素
- 增将元素转换成节点 (只有一个元素时:头尾都是指向同一个元素)add(int index,E ele)
- 在中间添加没有返回值,在尾部添加返回boolean
- 删除元素 remove(int index):E 将删除掉的节点元素返回
- 增将元素转换成节点 (只有一个元素时:头尾都是指向同一个元素)add(int index,E ele)
-
查找元素: get(int index):E
- 如果查找的元素下标小于元素长度的一半时,则是从头节点顺序查找元素
- 如果查找的元素下标大于元素长度的一半时,则是从尾节点逆向查找元素
- 双向链表的查询效率高于单向链表,原因是:双向链表是对半-二分法查找
-
修改元素 set(int index,E ele)
- 用新元素替换指定位置的元素,并且将原来的元素返回
面试题:
- 用新元素替换指定位置的元素,并且将原来的元素返回
-
ArrayList和LinkedList的区别
- 数据结构实现:ArrayList :基于数组,容量不够时候采用复制方式扩容。LinkedList:使用链表实现。
- 随机访问效率:ArrayList 比LinkedList在随机访问的时候效率要高,因为 LinkedList 是链表结构,需要依次查找元素,性能不高。
- 增加和删除效率 :LinkedList ⾸位操作具备很高效率。ArrayList 的头部性能稍差。
- 线程安全:ArrayList 和 LinkList 都是不同步的,不保证线程安全。
- 综合来说,需要频繁读取集合中的元素时,更推荐使用 Arrayist;而在头尾增删操作较多时,更推荐使用 LinkedList。ArrayList综合性能优秀,优选之!
- 时间复杂度: 数据结果/算法性能高低的不断依据
- O(1) -数组根据下标查询的时间复杂度
- O(n) ----单向链表的时间复杂度
- O(n^2) ----for循环俩层嵌套
-
注意点:
- 从JDK1.7开始,LinkedList的数据结构为双向链表
- 从JDK1.7之前,LinkedList的数据节后为双向循环链表
递归:
- 什么是递归:
- 是一种思想,运用到编程中体现为方法调方法本身
- 经典案例:
- 案例1:求阶乘
1! 1
2! 1*2 2*1!
3! 1*2*3 3*2!
4! 1*2*3*4 4*3!
5! 1*2*3*4*5 5*4!
n! n*(n-1)!
public class Demo(){
public static void main(String[] args){
System.out.println("请输入整数")
Scanner scanner = new Scanner(System.in)
int x =csanner.nextInt();
int resul = f(x)
System.out.println(result);
}
//求阶乘
public static int f(int n){
if(n==1)
return 1;
return n*f(n-1);
}
}
- 案例2:菲波那切数列(Fibonacci)
案例2:求出数列中某个位置的值
1 1 2 3 5 13 21 34 55...
使用递归实现: (位置从一开始)
int f(int n){
if(n==1|| n==2)
return 1;
return f(n-1)+f(n-2);
}
递归注意点:
- 1.递归必修有出口,否则会栈内存溢出 (StackOverflowError —SOF)
- 2.递归是有深度的,使用时深度不能太深,否则可能会造成栈内存溢出
- : SOF: 栈内存溢出
- OOM堆内存溢出 OutofMenoryError
- 内存泄漏:分配出去的内存回收不回来,无法重复利用,这种现象叫做内存泄漏内存溢出:剩余的内存不足以分配给请求的资源,会造成内存溢出。
内存溢出的原因:2个- 创建的对象太大,超过堆内存的可用空间,直接溢出
- 内存泄漏的一直累积,最终会造成内存溢出。
树
- 树是数据结构,是由若干个节点构成,有且仅有一个根节点,每个节点可以有若干个子节点。
- 专业术语:
- 高度:树的层数
- 度: 树中每个节点最多的子节点数
- 叶子节点:度为0的节点
- 根节点:树中最顶层的,尤其只有一个,位于树的最顶端
二叉树
- 度为2的数称为二叉树,所有的节点<=2,而不是每个节点的度必须为2
二叉排序树
- 什么是二拆排序树
- 二叉排序树(Binary Sort Tree),又称二叉查找树(Binary search Tree),亦称二叉搜索
树。是数据结构中的一类。在一般情况下,查询效率比链表(单向链表)结构要高。
- 二叉排序树(Binary Sort Tree),又称二叉查找树(Binary search Tree),亦称二叉搜索
- 特点
- 元素不重复(在Java中不可以重复,可视化网站可重复)
- 左子树上的所有节点均小于根节点
- 右子树上的所有节点均大于根节点
- 数据结构可视化网站:https://www.cs.usfca.edu/~galles/visualization/BST.html
- 二叉排序实现了排序
- 添加元素
- 和根节点比较大小,小于,添加到左子树,若左子树已经有节点,则再和节点继续比较,同理小于到该节点的左子树,大于到该节点的右子树。
- 二叉排序树中元素必须是可比较大小,如何保证?
- 让元素所属的类实现Comparablie接口或者comparator接口,定义好接口比较规则
- 比较器的接口:
- Comparable–内比较器
- 抽象方法ComparaeTo(T t):在该方法中定义比较规则
- Comparable–内比较器
- 案例:
在案例中,要求按照学生的姓名进行升序排列
//重写该方法
public int compareTo(student o) {
return name.compareTo(o.getName());
}
// 注意:若比较规则是根据引用类型进行比较,应调用引用类型的compareTo方法进行比较,返回int
//注意: compareTo方法中,引用类型(包括包装类型)的数据用于比较大小时,一定要用该类型的compareto方法,否则可能会出现错误。
//eg:Integer常量池,会缓存-128-127之间的数字,若Integer对象的值为该区间,则复用常量池中的对象,不会新创建;若Integer对象的值不在该区间,则每次都会重新new Integer对象
Comparator–外比较器
- 抽象方法: 在方法中定义比较规则
- 使用场景:当一个类中已经定义好比较规则,现在不想用原来的比较规则,想使用新的/临时的比较规则,此时就可以使用外比较器来完成。
/**
*假设有一个学生类:(年龄,姓名,学号,体重),已经按照学号和体重排序
*新需求: 按照学生年龄进行降序排序(不改变默认的比较规则)
*/
// 一般情况我们会在类的内部进行写入一个匿名内部类,根据年龄进行排序
Collections.sort(list,new Collection<Student>(
@Override
pulic int compare(Student o1,Student o2){
return o2.getAge().compareTo(o1.getAge());
}
});
二叉排序树基本操作
- 添加元素
1.和节点元素比较大小--使用compareTo方法来比较,看结果,是>0还是<0还是==0
2.将元素封装成节点添加到树中--需要定义内部类Node来表示节点,
元素本身 左子树 右子树
其属性有:E ele,Node left , Node right
3. 定义public boolean add(E e){}
3.1 判断根节点是否为null,
-若为null,直接将元素封装成节点称为'root'(为二叉排序树的必备属性)
-若不为null,从'root'开始尝试添加到某个节点上(从root开始以此尝试)
-先判断和节点的data是否相等,相等,返回false,添加失败
- 不相等,判断是否大于,若大于成为右子树,但是需要判断
- 若right为null ,将新元素封装为节点成为right
- 若right不为null,将新元素尝试添加到right上,使用递归,right.append(ele)
- 左子树同理
二叉排序树的遍历
- 先序遍历:
- 中序遍历: 左根右: 从根节点开始依次左根右,只要left不为null,就对left节点进行中序遍历,得到的结果为升序排序后的结果
- 后序遍历:左右根:
- 输出引用,将二叉排序树中的元素升序输出
- 重写toString方法
若没有元素(root==null),此时返回"[]’
若有元素,此时输出"[1,2,55,34]"
-使用StiringBuilder完成中序遍历过程中的元素拼接
查询某元素
- 若root为null时,则直接返回
- 若root不为null,且查找的元素不存在,此时需要返回null
- 若root不为null,查找元素是存在的,最终返回该元素对应的节点对象
删除元素
-
删除的是叶子节点,此时直接删除即可,让其父节点指向该节点的引用置为null,即可
-
删除的节点只有一颗子树,让该节点的父节点的left/right,指向删除节点的子节点即可
-
删除的节点有两颗子树,让删除节点的前驱节点/后继节点来替换该节点,才能保证树已让是有序的
- 前驱/后继节点: 是指中序拍下后,删除元素前一个节点为前驱,后一个节点为后继
-
二叉排序树极端情况下的现象
- 失衡二叉树 – 失去了二叉排序树的优点,失衡是我们要避免的
- 失衡二叉树 – 失去了二叉排序树的优点,失衡是我们要避免的
-
为了避免失衡二叉树,对二叉排序树进行了优化,产生了两种新的数据结构
- 平衡二叉树 -AVL树 ;要求左右子树的高度差<=1,若大于1则进行旋转
红黑树 (第二种优化)
什么是红黑树:
- 红黑树是实现了自平衡的二叉排序树
- 注意点:红黑树达到的不是绝对平衡(高度差是<=1) ,而是相对平衡(高度差可以大于1)
- 红黑树中的所有节点不是黑色就是红色
- 红黑树保证均衡必须遵守5个原则
- 红黑树是通过旋转(左旋,右旋)和改变节点的颜色来保持树的平衡的
- 节点是红色或者黑色
- 根节点必须是黑色
- 所有叶子节点都是黑色的(叶子节点是NIL节点)
- 每个红色的两个子节点都是黑色的(从每个叶子到跟的所有路径上不能有连续的两个红色节点)
- 从任意一个节点到其每个叶子节点所经历的简单路径上包含的黑色节点的个数相同(简称:黑高)
- 节点的颜色
- 根节点的颜色一定是黑色
- 新添加的节点颜色一定是红色,后续过程中可能会改变
- 红黑树的应用:TreeMap
- TreeMap会根据key-value中的key进行排序,形成自平衡二叉排序树
- 输出TreeMap的引用,输出的是排序的结果(根据key进行排序)
- Set和Map的关系
- 所有set集合的底层都是对应的Map集合
代码实现二叉树排序
- 不包含删除
package 二叉排序树;
import java.util.Collection;
/**
* 二叉排序树
*
* @param <E> 让元素实现Comparable接口
*/
public class BinarySearchTree<E extends Comparable<E>> {
//定义根节点
Node root;
//添加元素
public boolean add(E ele) {
if (root == null) {
root = new Node(ele);
return true;
}
//若root不为null ,将新元素尝试添加到节点上
return root.append(ele);
}
/**
* get(E e): Node 查找目标元素的节点,若存在,返回节点,不存在则返回null
* @param e
*/
public Node get(E e){
//判断root是否为null
if (root==null)
return null;
//root 不为null isDest:目标节点
return root.isDest(e);
}
@Override
public String toString() {
//先判断root是否为null
if (root == null)
return "[]";
//若不为null,进行中序遍历(左根右)
StringBuilder builder = new StringBuilder("[");
builder = root.midOrder(builder);
//需要对builder 进行去掉, 拼接 ]
builder.deleteCharAt(builder.lastIndexOf(",")).append("]");
return builder.toString();
}
//定义内部类 E 代表元素本身 Node 左或右子树
private class Node {
E data;
Node left;
Node right;
//在添加之前必须包装成节点才能装进来,则需要调用节点的构造方法
Node(E ele) {
this.data = ele;
}
/**
* 用于节点添加元素
* 思路:判断ele和当前节点的值是否相等,若相等,则返回false,添加失败;
* 若不相等,
* 则判断大于还是小于,大于则添加到right上,需要判断right是否为nuLl;为null,
* 直接让新元素成员right节点;若不为null,需要再想right上添加该元素;
* left节点同理
*
* @param ele
* @return
*/
public boolean append(E ele) {
if (ele.compareTo(data) == 0) {
right = new Node(ele);
return true;
//成为right,需要判断right是否为null
} else if (ele.compareTo(data) > 0) {
if (right == null) {
right = new Node(ele);
return true;
} else {
return root.append(ele);
}
} else {
//成为左子树
if (left == null) {
left = new Node(ele);
return true;
} else {
return left.append(ele);
}
}
}
//中序遍历:左根右
public StringBuilder midOrder(StringBuilder builder) {
//左
if (left != null)
left.midOrder(builder);
//中
builder.append(data).append(",");
//右
if (right != null)
left.midOrder(builder);
//以上三步结束,所有元素都已经保存到builder中
return builder;
}
//判断节点是否为要查找的节点
public Node isDest(E e) {
//判断当前节点的元素是否与e相等,若相等则直接返回
if (e.compareTo(data)==0)
return this;
//若e>当前节点元素,则到右子树上查找
else if (e.compareTo(data)>0){
//判断right是否为null
if (right==null){
return null;
}
return right.isDest(e);
}else {
//若e<当前节点元,则到左子树上查找
if (left==null)
return null;
return left.isDest(e);
}
}
//因为是在节点上查找,需要重写
@Override
public String toString() {
return "Node{" +
"data=" + data +
", left=" + left +
", right=" + right +
'}';
}
}
}