我的学习资料:
视频:尚硅谷Java数据结构与java算法(Java数据结构与算法)
书籍:《大话数据结构》
笔记中包括学习的内容,代码,同时自己总结了知识点速记(部分会带页内跳转,可点击跳转)供快速回顾和记忆学到的知识点。
跳表维护平衡性:随机函数
红黑树(以及AVL树)维护平衡性:左右旋
二叉排序树(二叉查找树)的定义
左子树仅包含小于当前结点的值
右子树仅包含大于当前结点的值
左右子树每个也必须是二叉查找树
优点:
中序遍历即可从小到大输出,理想状态下增删查时间复杂度O(logn),
缺点:
极端情况下倾斜,退化为链表,查的复杂度变为O(n)
因此出现了
二叉平衡树(AVL)
任何节点的左右子树的高度差不大于1的二叉查找树,是一种高度平衡的二叉查找树
因为增删查的操作都与树的高度挂钩,因此二叉平衡树就是通过左右旋来保证左右子树的高度差不多,使得树的高度接近O(logn),防止性能的退化。(一棵及其平衡的二叉树的高度大约是log2n)
红黑树
二叉平衡树的一种,严格的二叉平衡树维护平衡的代价很高,很复杂,因此弱平衡的红黑树,或者说近似平衡经常被使用到
定义:
选择跳表而不是红黑树:
5. 跳表的性能与红黑树近似,但是跳表代码实现更简单
6. 有序集合除了插入、删除、查询,还有一个范围查询的功能,由于跳表的索引可以很快的定位范围,在这个范围遍历输出即可,而红黑树的范围输出性能会弱一些
Hash散列表快速复习
通过哈希函数,将键映射到数组对应的位置中
hash冲突解决方式:开放寻址或者拉链法
开放寻址法中最简单是线性探测:一旦有冲突就往下找,直到找到空位为止(ThreadLocalMap就用的这个)
然后是二次探测,步长变长,+1 +2 变成+1的平方 +2的平方
双重散列:多订几个哈希函数,第一个有冲突就用第二个
拉链法:如果遇到冲突,就以链表的形式坠到hash桶后面
装载因子:填入表中的元素个数/散列表的长度
装载因子越大,空闲位置越少,冲突越多
打造工业级的散列表
装载因子过大了怎么办 -> 扩容
散列表扩容:容量变为2倍,数据进行搬移,一次性搬移延迟较高,可以分摊,扩容时不搬移旧数据,插入新数据时先插入新表,同时搬移一条旧数据,期间的查询,先在新表中查,查不到再去旧表。
选择冲突解决方法
开放寻址法:
优点:
4. 散列表中的数据都存在数组中,可以有效利用CPU加快查询速度
5. 序列化简单(链表中有指针,序列化不容易)
缺点:
6. 删除数据麻烦
7. 冲突的代价更高,装载因子上限不能太大,因此浪费内存空间
数据量小,装载因子小,用开放寻址法——Java的ThreadLocalMap使用开放寻址来解决散列冲突的原因
拉链法(链表法)
优点:
1.内存利用率高,不需要提前申请空间
2.对大装载因子的容忍度高
缺点:
1.额外耗费内存,要存储指针,对小对象来说比较消耗内存
2.链表节点零散分布在内存中,不连续,对CPU缓存不友好
大对象、大数据量的散列表,用拉链法,并且它支持更多的优化策略,比如用红核数代替链表
HashMap分析
默认大小:16
装载因子和动态扩容:0.75,当HashMap中元素个数超过0.75*capacity(capacity是散列表的容量)就会扩容,扩容为两倍的大小。
散列冲突解决方法:
JDK1.8之前:数组+链表
JDK1.8之后:数组+链表+红黑树
当链表长度大于8之后,就会变为红黑树,因为数据量小的时候,红黑树维护平衡性的策略是左右旋,比起链表,性能优势不明显
散列函数:
int hash(Object key) {
int h = key.hashCode();
return (h ^ (h >>> 16)) & (capitity -1); //capicity 表示散列表的大小
}
其中,hashCode() 返回的是 Java 对象的 hash code。比如 String 类型的对象的 hashCode()就是下面这样:
public int hashCode() {
int var1 = this.hash;
if(var1 == 0 && this.value.length > 0) {
char[] var2 = this.value;
for(int var3 = 0; var3 < this.value.length; ++var3) {
var1 = 31 * var1 + var2[var3];
}
this.hash = var1;
}
return var1;
}
因此设计一个工业级别的散列表需要考虑:
堆排序的基本思想是:
要求:给你一个数组 {4,6,8,5,9} , 要求使用堆排序法,将数组升序排序。
步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。原始的数组 [4, 6, 8, 5, 9]
(i-1)/2
】,左孩子的编号为【2i+1
】,右孩子的编号为【2i+2
】。从下到上,从右到左处理第一个非叶结点:
二叉树用顺序存储的数组实现,标号从0开始
循环调整为大顶堆(adjustHeap)的过程:
调整为大顶堆的过程类似将最开始的根值temp备份视为第一个要调整的位置,然后从其孩子开始看,如果temp值比大孩子的值小,那么孩子的值先拽过去覆盖原来要调整的位置,同时这个孩子的位置变为下一个待调整位置,从孩子的孩子继续看,一直这样有大的就拽上来,最终一直到temp大于它的左右孩子结点,符合大顶堆的定义,那么就可以退出循环,将temp值放入上一次的待调整位置。
public class HeapSort {
public static void main(String[] args) {
int[] arr ={4, 6, 8, 5, 9};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void heapSort(int[] arr){
//从右到左,从下到上的第一个非叶结点开始先调整为一个大顶堆
for (int i=arr.length/2-1;i>=0;i--){
adjustHeap(arr,i,arr.length-1);
}
//调整后将大顶堆的顶端与当前末尾交换位置,继续调整为大顶堆
for(int j=arr.length-1;j>0;j--){
int temp = arr[0];
arr[0] = arr[j];
arr[j] = temp;
adjustHeap(arr,0,j-1);
}
}
//将数组arr[],非叶结点标号i,元素最大标号length
public static void adjustHeap(int[] arr,int i,int length){
//i指向【要调整的位置】,开始是根结点的位置
int temp = arr[i];//将根结点的值进行存储
//从2*i+1是标号i的左孩子,k指向值较大的左孩子或者右孩子
for (int k=2*i+1;k<=length;k=k*2+1){
if (k+1<=length && arr[k+1]>arr[k]){
k++;//如果右孩子大于左孩子,那么k指向右孩子
}
//如果左或右孩子的值大于根的值,不符合大顶堆,需要调整
//将孩子值赋值给待调整位置的值,相当于我们对【以孩子为根的树】进行了调整,所以将i指向孩子的位置k
//进行下一轮调整
if (arr[k]>temp){
arr[i]=arr[k];
i=k;
}else {
break;//如果左或右孩子的值小于根的值,符合大顶堆,无需调整,退出循环
}
}
//循环过后,i已经指向了最后的待调整位置,将temp放入
arr[i]=temp;
}
}
public class HuffmanTreeTest {
public static void main(String[] args) {
int[] arr={ 13, 7, 8, 3, 29, 6, 1};
TreeNode l1 = huffManTree(arr);
l1.preOrder();
}
public static TreeNode huffManTree(int[] arr){
//遍历arr数组,将arr中的每一个元素变成一个树结点
List<TreeNode> treenodes = new ArrayList<TreeNode>();
for (int i:arr
) {
treenodes.add(new TreeNode(i));
}
//循环,每次取前两个最小的元素变成一个树结点
while(treenodes.size()>1){
//排序,从小到大
Collections.sort(treenodes);
//取出权值最小和第二小的节点作为左右结点
TreeNode leftNode=treenodes.get(0);
TreeNode rightNode= treenodes.get(1);
//将左右结点的值相加作为新的结点
TreeNode newNode = new TreeNode(leftNode.val+rightNode.val);
//将左右结点放到新结点后面
newNode.left=leftNode;
newNode.right=rightNode;
//从集合中删除刚刚的左右结点,并加入新结点进行下一轮排序
treenodes.remove(leftNode);
treenodes.remove(rightNode);
treenodes.add(newNode);
}
return treenodes.get(0);
}
}
class TreeNode implements Comparable<TreeNode> {
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val) {
this.val = val;
}
public void preOrder(){
System.out.println(this);
if (this.left!=null) this.left.preOrder();
if (this.right!=null) this.right.preOrder();
}
@Override
public String toString() {
return "Node["+val+"]";
}
@Override
public int compareTo(TreeNode o) {
return this.val-o.val;
}
}
给你一个数列 (7, 3, 10, 12, 5, 1, 9),要求能够高效的完成对数据的查询和添加
二叉排序树BST (Binary Sort(Search) Tree):对于二叉排序树的 任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点
比如针对前面的数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树为:
一个数组创建成对应的二叉排序树,并使用中序遍历二叉排序树,比如: 数组为 Array(7, 3, 10, 12, 5, 1, 9) , 创建成对应的二叉排序树为 :
public class binarySortTreeTest {
public static void main(String[] args) {
int[] arr ={7, 3, 10, 12, 5, 1, 9};
BSTNode t1 =new BSTNode(arr[0]);
for (int i=1;i<arr.length;i++){
t1.add(new BSTNode(arr[i]));
}
//二叉排序树的中序遍历
t1.infixOrder();
}
}
//二叉排序树结点类
class BSTNode{
int val;
BSTNode left;
BSTNode right;
public BSTNode(int val) {
this.val = val;
}
@Override
public String toString() {
return "BSTNode{" +
"val=" + val +
'}';
}
//添加二叉排序树的结点
public void add(BSTNode node){
if (node==null) return;
if (node.val<this.val){
if (this.left==null){
this.left =node;
}else{
this.left.add(node);
}
}else {
if (this.right==null){
this.right=node;
}else{
this.right.add(node);
}
}
}
//中序遍历
public void infixOrder(){
if (this.left!=null) this.left.infixOrder();
System.out.println(this);
if (this.right!=null) this.right.infixOrder();
}
}
二叉排序树的删除情况比较复杂,有下面三种情况需要考虑
第二种情况: 删除只有一颗子树的节点 比如 1
思路
(1) 需求先去找到要删除的结点 targetNode
(2) 找到 targetNode 的 父结点 parent
(3) 确定 targetNode 的子结点是左子结点还是右子结点
(4) targetNode 是 parent 的左子结点还是右子结点
(5) 如果 targetNode 有左子结点
5. 1 如果 targetNode 是 parent 的左子结点
parent.left = targetNode.left;
5.2 如果 targetNode 是 parent 的右子结点
parent.right = targetNode.left;
(6) 如果 targetNode 有右子结点
6.1 如果 targetNode 是 parent 的左子结点
parent.left = targetNode.right;
6.2 如果 targetNode 是 parent 的右子结点
parent.right = targetNode.right
情况三 : 删除有两颗子树的节点. (比如:7, 3,10 )
思路
(1) 需求先去找到要删除的结点 targetNode
(2) 找到 targetNode 的 父结点 parent
(3) 从 targetNode 的右子树找到最小的结点
(4) 用一个临时变量,将 最小结点的值保存 temp = 11
(5) 删除该最小结点
(6) targetNode.value = temp
public class binarySortTreeTest {
public static void main(String[] args) {
int[] arr ={7, 3, 10, 12, 5, 1, 9,2};
BSTNode t1 =new BSTNode(arr[0]);
for (int i=1;i<arr.length;i++){
t1.add(new BSTNode(arr[i]));
}
//二叉排序树的中序遍历
t1.infixOrder();
//测试删除叶子结点
// t1.deleteBSTNode(2);
// t1.infixOrder();
//测试删除一个子树的结点
// t1.deleteBSTNode(1);
// t1.infixOrder();
//测试删除两个子树的结点
t1.deleteBSTNode(7);
t1.infixOrder();
}
}
//二叉排序树结点类
class BSTNode{
int val;
BSTNode left;
BSTNode right;
public BSTNode(int val) {
this.val = val;
}
@Override
public String toString() {
return "BSTNode{" +
"val=" + val +
'}';
}
//添加二叉排序树的结点
public void add(BSTNode node){
if (node==null) return;
if (node.val<this.val){
if (this.left==null){
this.left =node;
}else{
this.left.add(node);
}
}else {
if (this.right==null){
this.right=node;
}else{
this.right.add(node);
}
}
}
//中序遍历
public void infixOrder(){
if(this==null) System.out.println("二叉树为空~~");
if (this.left!=null) this.left.infixOrder();
System.out.println(this);
if (this.right!=null) this.right.infixOrder();
}
//查找目标结点
public BSTNode searchTarget(int value){
if (this==null) return null;
if (this.val==value){
return this;
}else if(this.val>value){
if (this.left!=null){
return this.left.searchTarget(value);
}else{
return null;
}
}else{
if (this.val<value){
return this.right.searchTarget(value);
}else {
return null;
}
}
}
public BSTNode searchPerant(int value){
if (this==null) return null;
if ((this.left!=null&&this.left.val==value)||
(this.right!=null&&this.right.val==value)){
return this;
}else{
if (this.val>value && this.left!=null){
return this.left.searchPerant(value);
}else if(this.val<value && this.right!=null){
return this.right.searchPerant(value);
}else{
return null;
}
}
}
//返回以node为根结点的二叉排序树的最小结点的值
//同时删除以node为根结点的二叉排序树的最小结点
public int delRightTreeMin(BSTNode node ){
BSTNode target =node;
while (target.left!=null){
target =target.left;//二叉排序树的左孩子一定比自己小,这样一直循环找到最小
}
//此时target指向最小值结点,删除最小结点
deleteBSTNode(target.val);
return target.val;
}
public void deleteBSTNode(int value){
if (this==null) {
return;
}else{
//先去寻找要删除的结点 targetNode
BSTNode targetNode = searchTarget(value);
if (targetNode==null){
return;
}
BSTNode parentNode = searchPerant(value);
if (parentNode==null && targetNode.left==null && targetNode.right==null){
targetNode=null;//如果目标结点存在,目标结点的父结点和左右孩子都不存在,证明是只有一个结点的树,将它删除即可
}
//第一种情况:如果目标结点没有左右孩子,证明是叶子结点
if (targetNode.right==null && targetNode.left==null){
//判断target是parent的左孩子还是右孩子
if (parentNode.left==targetNode){
parentNode.left =null;
}else {
parentNode.right =null;
}
}else if(targetNode.right!=null && targetNode.left!=null){//第二种情况:目标结点有两棵子树
//调用方法:delRightTreeMin,删除目标结点的最小值结点,同时将最小值结点的值赋值给目标结点
int minVal = delRightTreeMin(targetNode.right);
targetNode.val=minVal;
}else{//第三种情况:目标结点只有一棵子树
if (targetNode.left!=null){//目标结点只有左子树
if (parentNode!=null) {//目标结点如果有双亲
if (parentNode.left == targetNode) {//目标结点是双亲的左孩子
parentNode.left = targetNode.left;//双亲左孩子变目标结点的左子树
} else {//目标结点是双亲的右孩子
parentNode.right = targetNode.left;//双亲右孩子变目标结点的左子树
}
}else{//目标结点没有双亲则将根赋值给它的子树
//将根赋值给它的子树 root =targetNode.left;
}
}else {//目标结点只有右子树
if (parentNode!=null) {
if (parentNode.left == targetNode) {//目标结点是双亲的左孩子
parentNode.left = targetNode.right;//双亲左孩子变目标结点的右子树
} else {//目标结点是双亲的右孩子
parentNode.right = targetNode.right;//双亲右孩子变目标结点的右子树
}
}else{
//将根赋值给它的子树 root =targetNode.right;
}
}
}
}
}
}
给你一个数列{1,2,3,4,5,6},要求创建一颗二叉排序树(BST), 并分析问题所在.
平衡二叉树构建的基本思想就是在构建二叉排序树的过程中,每当插入一个结点时,先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡子树。在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树。
为了能在讲解算法时轻松-一些,我们先讲一个平衡二叉树构建过程的例子。假设我们现在有一一个数组 a[10]={3,2,1,4,5,6,7,10,9,8}需要构建 二叉排序树。在没有学习平衡二叉树之前,根据二叉排序树的特性,我们通常会将它构建成如图8-7-4的图1所示的样子。虽然它完全符合二叉排序树的定义,但是对这样高度达到8的二叉树来说,查找是非常不利的。我们更期望能构建成如图8-7-4的图2的样子,高度为4的二叉排序树才可以提供高效的查找效率。那么现在我们就来研究如何将一个数组构建出图2的树结构。
对于数组a[10]={3,2,1,4,5,6,7,10,9,8}的前两位3和2,我们很正常地构建,到了第3个数“1”时,发现此时根结点“3” 的平衡因子变成了2,此时整棵树都成了最小不平衡子树,因此需要调整,如图8-7-5 的图1 (结点左上角数字为平衡因子BF值)。因为BF值为正,因此我们将整个树进行右旋(顺时针旋转),此时结点2成了根结点,3成了2的右孩子,这样三个结点的BF值均为0,非常的平衡,如图8-7-5 的图2所示。
然后我们再增加结点4,平衡因子没发生改变,如图3。
增加结点5时,结点3的BF值为-2,说明要旋转了。由于BF是负值,所以我们对这棵最小平衡子树进行左旋(逆时针旋转),如图4,此时我们整个树又达到了平衡。
继续,增加结点6时,发现根结点2的BF值变成了-2,如图8-7-6的图6。所以我们对根结点进行了左旋,注意此时本来结点3是4的左孩子,由于旋转后需要满足二叉排序树特性,因此它成了结点2的右孩子,如图7。增加结点7,同样的左旋转,使得整棵树达到平衡,如图8和图9所示。
平衡因子>1,右旋转
平衡因子<-1,左旋转
//获取当前结点的高度,以该结点为根结点的树的高度
public int height(){
return Math.max(left==null?0:left.height(),right==null?0:right.height())+1;
}
然后计算左右子树的高度:
//获取左子树的高度
public int getLeftHeight(){
if (left==null) return 0;
return left.height();
}
//获取右子树的高度
public int getRightHeight(){
if (right==null) return 0;
return right.height();
}
编写左旋的方法:
// P 当前结点 PL 当前结点的左子树
//左旋转方法
// P newNode=P P newNode=P R(P变为R) R(P变为R)
// / \ / \ \ / \ \ / \
// PL R -> PL RL R -> PL RL \ ×R(被孤立的样子) ----> newNode=P RR
// / \ \ \ / \ \
// RL RR RR RR PL RL N
// \ \N \N (恢复平衡)
// N(插入破坏了平衡)
//新建一个与当前结点值相同的结点 newNode
public void leftRotate(){
//以当前结点的值创建一个新结点
AVLTreeNode newNode =new AVLTreeNode(this.val);
//新结点的左子树是当前结点的左子树
newNode.left=this.left;
//新结点的右子树,是当前结点右子树的左子树
newNode.right = this.right.left;
//当前结点的值变为它右孩子的值
this.val=this.right.val;
//当前结点的右子树变为它右孩子的右子树
this.right=this.right.right;
//当前结点的左子树变为新结点
this.left = newNode;
}
在node类的add方法中加入判断需要左旋的代码:
//如果平衡因子<-1,需要左旋
if(this.getLeftHeight()-this.getRightHeight()<-1){
leftRotate();
}
这样的左旋判断其实有问题,先这样写,然后测试:
public static void main(String[] args) {
int[] arr = {4, 3, 6, 5, 7, 8};
AVLTreeNode t1 = new AVLTreeNode(arr[0]);
for (int i = 1; i < arr.length; i++) {
t1.add(new AVLTreeNode(arr[i]));
}
//中序遍历
t1.infixOrder();
System.out.println("树的高度为"+t1.height());
System.out.println("左子树的高度为"+t1.getLeftHeight());
System.out.println("右子树的高度为"+t1.getRightHeight());
}
//如果bf值大于1,进行右旋转
//相当于要往左拎一个,增加右子树的高度
public void rightRotate(){
//以当前结点的值创建一个新结点
AVLTreeNode newNode =new AVLTreeNode(this.val);
//新结点的右子树是当前结点的右子树
newNode.right = this.right;
//新结点的左子树是当前结点左子树的右子树
newNode.left =this.left.right;
//当前结点的值变为左孩子的值
this.val=this.left.val;
//当前结点的左子树变为它左孩子的左子树
this.left =this.left.left;
//当前结点的右子树变为新结点
this.right =newNode;
}
在node类的add方法中加入判断需要右旋的代码,这样的右旋判断其实有问题,先这样写,
//如果平衡因子>1,需要右旋
if(this.getLeftHeight()-this.getRightHeight()>1){
rightRotate();
}
加入右旋后测试右旋转是否正确:
public static void main(String[] args) {
// int[] arr = {4,3,6,5,7,8};
int[] arr = {10,12, 8, 9, 7, 6};
AVLTreeNode t1 = new AVLTreeNode(arr[0]);
for (int i = 1; i < arr.length; i++) {
t1.add(new AVLTreeNode(arr[i]));
}
//中序遍历
t1.infixOrder();
System.out.println("树的高度为"+t1.height());
System.out.println("左子树的高度为"+t1.getLeftHeight());
System.out.println("右子树的高度为"+t1.getRightHeight());
}
前面的两个数列,进行单旋转(即一次旋转)就可以将非平衡二叉树转成平衡二叉树,但是在某些情况下,单旋转
不能完成平衡二叉树的转换。比如数列
int[] arr = { 10, 11, 7, 6, 8, 9 }; 运行原来的代码可以看到,并没有转成 AVL 树.
int[] arr = {2,1,6,5,7,3}; // 运行原来的代码可以看到,并没有转成 AVL 树
需要左旋时同理对称:
//如果平衡因子<-1,需要左旋
if(this.getLeftHeight()-this.getRightHeight()<-1){
if (this.right!=null &&this.right.getLeftHeight()>this.right.getRightHeight()){
this.right.rightRotate();
this.leftRotate();
}else{
this.leftRotate();
}
return;
}
//如果平衡因子>1,需要右旋
if(this.getLeftHeight()-this.getRightHeight()>1){
if (this.left!=null && this.left.getRightHeight()>this.left.getLeftHeight()){
this.left.leftRotate();
this.rightRotate();
}else{
this.rightRotate();
}
return;
}
进行测试:
public static void main(String[] args) {
// int[] arr = {4,3,6,5,7,8};
// int[] arr = {10,12, 8, 9, 7, 6};
int[] arr = {10,11,7,6,8,9};
// 10 10 8
// /\ 左结点 /\ 当前结点 / \
// 7 11 ----> 8 11 ----> 7 10
// / \ 左旋 / \ 右旋 / / \
// 6 8 7 9 6 9 11
// \ /
// 9 6
AVLTreeNode t1 = new AVLTreeNode(arr[0]);
for (int i = 1; i < arr.length; i++) {
t1.add(new AVLTreeNode(arr[i]));
}
//中序遍历
t1.infixOrder();
System.out.println("树的高度为"+t1.height());
System.out.println("左子树的高度为"+t1.getLeftHeight());
System.out.println("右子树的高度为"+t1.getRightHeight());
}