数据结构——树基本知识(遍历,堆排序,赫夫曼树,BST,AVL,B树,B+,B*,2-3树)

1 基础(遍历、查找、删除、顺序存储)

1.1 结点类与二叉树类的定义

1.1.1 结点类
//先创建HeroNode 结点
class HeroNode {
	private int no;
	private String name;
	private HeroNode left; //默认null
	private HeroNode right; //默认null
	public HeroNode(int no, String name) {
		this.no = no;
		this.name = name;
	}
	public int getNo() {
		return no;
	}
	public void setNo(int no) {
		this.no = no;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public HeroNode getLeft() {
		return left;
	}
	public void setLeft(HeroNode left) {
		this.left = left;
	}
	public HeroNode getRight() {
		return right;
	}
	public void setRight(HeroNode right) {
		this.right = right;
	}
	@Override
	public String toString() {
		return "HeroNode [no=" + no + ", name=" + name + "]";
	}
	
	//递归删除结点
	//1.如果删除的节点是叶子节点,则删除该节点
	//2.如果删除的节点是非叶子节点,则删除该子树
	public void delNode(int no) {
		
		//思路
		/*
		 * 	1. 因为我们的二叉树是单向的,所以我们是判断当前结点的子结点是否需要删除结点,而不能去判断当前这个结点是不是需要删除结点.
			2. 如果当前结点的左子结点不为空,并且左子结点 就是要删除结点,就将this.left = null; 并且就返回(结束递归删除)
			3. 如果当前结点的右子结点不为空,并且右子结点 就是要删除结点,就将this.right= null ;并且就返回(结束递归删除)
			4. 如果第2和第3步没有删除结点,那么我们就需要向左子树进行递归删除
			5.  如果第4步也没有删除结点,则应当向右子树进行递归删除.

		 */
		//2. 如果当前结点的左子结点不为空,并且左子结点 就是要删除结点,就将this.left = null; 并且就返回(结束递归删除)
		if(this.left != null && this.left.no == no) {
			this.left = null;
			return;
		}
		//3.如果当前结点的右子结点不为空,并且右子结点 就是要删除结点,就将this.right= null ;并且就返回(结束递归删除)
		if(this.right != null && this.right.no == no) {
			this.right = null;
			return;
		}
		//4.我们就需要向左子树进行递归删除
		if(this.left != null) {
			this.left.delNode(no);
		}
		//5.则应当向右子树进行递归删除
		if(this.right != null) {
			this.right.delNode(no);
		}
	}
	
	//编写前序遍历的方法
	public void preOrder() {
		System.out.println(this); //先输出父结点
		//递归向左子树前序遍历
		if(this.left != null) {
			this.left.preOrder();
		}
		//递归向右子树前序遍历
		if(this.right != null) {
			this.right.preOrder();
		}
	}
	//中序遍历
	public void infixOrder() {
		
		//递归向左子树中序遍历
		if(this.left != null) {
			this.left.infixOrder();
		}
		//输出父结点
		System.out.println(this);
		//递归向右子树中序遍历
		if(this.right != null) {
			this.right.infixOrder();
		}
	}
	//后序遍历
	public void postOrder() {
		if(this.left != null) {
			this.left.postOrder();
		}
		if(this.right != null) {
			this.right.postOrder();
		}
		System.out.println(this);
	}
	
	//前序遍历查找
	/**
	 * 
	 * @param no 查找no
	 * @return 如果找到就返回该Node ,如果没有找到返回 null
	 */
	public HeroNode preOrderSearch(int no) {
		System.out.println("进入前序遍历");
		//比较当前结点是不是
		if(this.no == no) {
			return this;
		}
		//1.则判断当前结点的左子节点是否为空,如果不为空,则递归前序查找
		//2.如果左递归前序查找,找到结点,则返回
		HeroNode resNode = null;
		if(this.left != null) {
			resNode = this.left.preOrderSearch(no);
		}
		if(resNode != null) {//说明我们左子树找到
			return resNode;
		}
		//1.左递归前序查找,找到结点,则返回,否继续判断,
		//2.当前的结点的右子节点是否为空,如果不空,则继续向右递归前序查找
		if(this.right != null) {
			resNode = this.right.preOrderSearch(no);
		}
		return resNode;
	}
	
	//中序遍历查找
	public HeroNode infixOrderSearch(int no) {
		//判断当前结点的左子节点是否为空,如果不为空,则递归中序查找
		HeroNode resNode = null;
		if(this.left != null) {
			resNode = this.left.infixOrderSearch(no);
		}
		if(resNode != null) {
			return resNode;
		}
		System.out.println("进入中序查找");
		//如果找到,则返回,如果没有找到,就和当前结点比较,如果是则返回当前结点
		if(this.no == no) {
			return this;
		}
		//否则继续进行右递归的中序查找
		if(this.right != null) {
			resNode = this.right.infixOrderSearch(no);
		}
		return resNode;
		
	}
	
	//后序遍历查找
	public HeroNode postOrderSearch(int no) {
		
		//判断当前结点的左子节点是否为空,如果不为空,则递归后序查找
		HeroNode resNode = null;
		if(this.left != null) {
			resNode = this.left.postOrderSearch(no);
		}
		if(resNode != null) {//说明在左子树找到
			return resNode;
		}
		
		//如果左子树没有找到,则向右子树递归进行后序遍历查找
		if(this.right != null) {
			resNode = this.right.postOrderSearch(no);
		}
		if(resNode != null) {
			return resNode;
		}
		System.out.println("进入后序查找");
		//如果左右子树都没有找到,就比较当前结点是不是
		if(this.no == no) {
			return this;
		}
		return resNode;
	}
	
}
1.1.2 树类
class BinaryTree {
	private HeroNode root;

	public void setRoot(HeroNode root) {
		this.root = root;
	}
	
	//删除结点
	public void delNode(int no) {
		if(root != null) {
			//如果只有一个root结点, 这里立即判断root是不是就是要删除结点
			if(root.getNo() == no) {
				root = null;
			} else {
				//递归删除
				root.delNode(no);
			}
		}else{
			System.out.println("空树,不能删除~");
		}
	}
	//前序遍历
	public void preOrder() {
		if(this.root != null) {
			this.root.preOrder();
		}else {
			System.out.println("二叉树为空,无法遍历");
		}
	}
	
	//中序遍历
	public void infixOrder() {
		if(this.root != null) {
			this.root.infixOrder();
		}else {
			System.out.println("二叉树为空,无法遍历");
		}
	}
	//后序遍历
	public void postOrder() {
		if(this.root != null) {
			this.root.postOrder();
		}else {
			System.out.println("二叉树为空,无法遍历");
		}
	}
	
	//前序遍历
	public HeroNode preOrderSearch(int no) {
		if(root != null) {
			return root.preOrderSearch(no);
		} else {
			return null;
		}
	}
	//中序遍历
	public HeroNode infixOrderSearch(int no) {
		if(root != null) {
			return root.infixOrderSearch(no);
		}else {
			return null;
		}
	}
	//后序遍历
	public HeroNode postOrderSearch(int no) {
		if(root != null) {
			return this.root.postOrderSearch(no);
		}else {
			return null;
		}
	}
}

1.2 三种遍历⭐⭐⭐

1.2.1 递归(dfs写在类中)
//编写前序遍历的方法
	public void preOrder() {
		System.out.println(this); //先输出父结点
		//递归向左子树前序遍历
		if(this.left != null) {
			this.left.preOrder();
		}
		//递归向右子树前序遍历
		if(this.right != null) {
			this.right.preOrder();
		}
	}
	//中序遍历
	public void infixOrder() {
		
		//递归向左子树中序遍历
		if(this.left != null) {
			this.left.infixOrder();
		}
		//输出父结点
		System.out.println(this);
		//递归向右子树中序遍历
		if(this.right != null) {
			this.right.infixOrder();
		}
	}
	//后序遍历
	public void postOrder() {
		if(this.left != null) {
			this.left.postOrder();
		}
		if(this.right != null) {
			this.right.postOrder();
		}
		System.out.println(this);
	}
1.2.2 迭代(堆栈)(144、145、94)

前序(最容易一个)

public List<Integer> preorderTraversal(TreeNode root) {
    List<Integer> ret = new ArrayList<>();
    Stack<TreeNode> stack = new Stack<>();
    stack.push(root);
    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        if (node == null) continue;
        ret.add(node.val);
        stack.push(node.right);  // 先右后左,保证左子树先遍历
        stack.push(node.left);
    }
    return ret;
}

后序,可以通过前序的变形得到,前序遍历为根左右,调整顺序为根右左,正好是后序的相反

public List<Integer> postorderTraversal(TreeNode root) {
    List<Integer> ret = new ArrayList<>();
    Stack<TreeNode> stack = new Stack<>();
    stack.push(root);
    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        if (node == null) continue;
        ret.add(node.val);
        stack.push(node.left);
        stack.push(node.right);
    }
    Collections.reverse(ret);
    return ret;
}

中序,不断遍历左子树压入堆栈,临时结点很巧妙

public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> ret = new ArrayList<>();
    if (root == null) return ret;
    Stack<TreeNode> stack = new Stack<>();
    TreeNode cur = root;
    while (cur != null || !stack.isEmpty()) {
        while (cur != null) {
            stack.push(cur);
            cur = cur.left;
        }
        TreeNode node = stack.pop();
        ret.add(node.val);
        cur = node.right;
    }
    return ret;
}

1.3 前中后查询指定结点,定义在结点类中(1.2的变形)

//前序遍历查找
	/**
	 * 
	 * @param no 查找no
	 * @return 如果找到就返回该Node ,如果没有找到返回 null
	 */
	public HeroNode preOrderSearch(int no) {
		System.out.println("进入前序遍历");
		//比较当前结点是不是
		if(this.no == no) {
			return this;
		}
		//1.则判断当前结点的左子节点是否为空,如果不为空,则递归前序查找
		//2.如果左递归前序查找,找到结点,则返回
		HeroNode resNode = null;
		if(this.left != null) {
			resNode = this.left.preOrderSearch(no);
		}
		if(resNode != null) {//说明我们左子树找到
			return resNode;
		}
		//1.左递归前序查找,找到结点,则返回,否继续判断,
		//2.当前的结点的右子节点是否为空,如果不空,则继续向右递归前序查找
		if(this.right != null) {
			resNode = this.right.preOrderSearch(no);
		}
		return resNode;
	}
	
	//中序遍历查找
	public HeroNode infixOrderSearch(int no) {
		//判断当前结点的左子节点是否为空,如果不为空,则递归中序查找
		HeroNode resNode = null;
		if(this.left != null) {
			resNode = this.left.infixOrderSearch(no);
		}
		if(resNode != null) {
			return resNode;
		}
		System.out.println("进入中序查找");
		//如果找到,则返回,如果没有找到,就和当前结点比较,如果是则返回当前结点
		if(this.no == no) {
			return this;
		}
		//否则继续进行右递归的中序查找
		if(this.right != null) {
			resNode = this.right.infixOrderSearch(no);
		}
		return resNode;
		
	}
	
	//后序遍历查找
	public HeroNode postOrderSearch(int no) {
		
		//判断当前结点的左子节点是否为空,如果不为空,则递归后序查找
		HeroNode resNode = null;
		if(this.left != null) {
			resNode = this.left.postOrderSearch(no);
		}
		if(resNode != null) {//说明在左子树找到
			return resNode;
		}
		
		//如果左子树没有找到,则向右子树递归进行后序遍历查找
		if(this.right != null) {
			resNode = this.right.postOrderSearch(no);
		}
		if(resNode != null) {
			return resNode;
		}
		System.out.println("进入后序查找");
		//如果左右子树都没有找到,就比较当前结点是不是
		if(this.no == no) {
			return this;
		}
		return resNode;
	}

1.4删除指定结点,递归,写在结点类

	思路
	  	1. 因为我们的二叉树是单向的,所以我们是判断当前结点的子结点是否需要删除结点,而不能去判断当前这个结点是不是需要删除结点.
		2. 如果当前结点的左子结点不为空,并且左子结点 就是要删除结点,就将this.left = null; 并且就返回(结束递归删除)
		3. 如果当前结点的右子结点不为空,并且右子结点 就是要删除结点,就将this.right= null ;并且就返回(结束递归删除)
		4. 如果第2和第3步没有删除结点,那么我们就需要向左子树进行递归删除
		5.  如果第4步也没有删除结点,则应当向右子树进行递归删除.
public void delNode(int no) {
		

		//2. 如果当前结点的左子结点不为空,并且左子结点 就是要删除结点,就将this.left = null; 并且就返回(结束递归删除)
		if(this.left != null && this.left.no == no) {
			this.left = null;
			return;
		}
		//3.如果当前结点的右子结点不为空,并且右子结点 就是要删除结点,就将this.right= null ;并且就返回(结束递归删除)
		if(this.right != null && this.right.no == no) {
			this.right = null;
			return;
		}
		//4.我们就需要向左子树进行递归删除
		if(this.left != null) {
			this.left.delNode(no);
		}
		//5.则应当向右子树进行递归删除
		if(this.right != null) {
			this.right.delNode(no);
		}
	}

1.5 顺序存储(数组存储完全二叉树)———>堆排序

顺序存储二叉树的特点:

  1. 顺序二叉树通常只考虑完全二叉树
  2. 第n个元素的左子节点为 2 * n + 1
  3. 第n个元素的右子节点为 2 * n + 2
  4. 第n个元素的父节点为 (n-1) / 2
  5. n : 表示二叉树中的第几个元素(按0开始编号)
//编写一个方法,完成顺序存储二叉树的前序遍历
	/**
	 * 
	 * @param index 数组的下标 
	 */
	public void preOrder(int index) {
		//如果数组为空,或者 arr.length = 0
		if(arr == null || arr.length == 0) {
			System.out.println("数组为空,不能按照二叉树的前序遍历");
		}
		//输出当前这个元素
		System.out.println(arr[index]); 
		//向左递归遍历
		if((index * 2 + 1) < arr.length) {
			preOrder(2 * index + 1 );
		}
		//向右递归遍历
		if((index * 2 + 2) < arr.length) {
			preOrder(2 * index + 2);
		}
	}
		//int[] arr = { 1, 2, 3, 4, 5, 6, 7 };
		//创建一个 ArrBinaryTree
		//ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
		//arrBinaryTree.preOrder(); // 1,2,4,5,3,6,7

1.6 线索化二叉树(有难度)

线索二叉树基本介绍:

  1. n个结点的二叉链表中含有n+1 【公式 2n-(n-1)=n+1】个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")
  2. 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种
  3. 一个结点的前一个结点,称为前驱结点。一个结点的后一个结点,称为后继结点
  4. left 指向的是左子树,也可能是指向的前驱节点。
  5. right指向的是右子树,也可能是指向后继节点。
    对二叉树进行中序线索化
//编写对二叉树进行中序线索化的方法
	/**
	* node的方法内
	 * node声明pre属性,前一个结点
	 * @param node 就是当前需要线索化的结点
	 */
	public void threadedNodes(HeroNode node) {
		
		//如果node==null, 不能线索化
		if(node == null) {
			return;
		}
		
		//(一)先线索化左子树
		threadedNodes(node.getLeft());
		//(二)线索化当前结点[有难度]
		
		//处理当前结点的前驱结点
		//以8结点来理解
		//8结点的.left = null , 8结点的.leftType = 1
		if(node.getLeft() == null) {
			//让当前结点的左指针指向前驱结点 
			node.setLeft(pre); 
			//修改当前结点的左指针的类型,指向前驱结点
			//遍历结点要用标志位
			node.setLeftType(1);
		}
		
		//处理pre的后继结点
		if (pre != null && pre.getRight() == null) {
			//让前驱结点的右指针指向当前结点
			pre.setRight(node);
			//修改前驱结点的右指针类型
			pre.setRightType(1);
		}
		//!!! 每处理一个结点后,让当前结点是下一个结点的前驱结点
		pre = node;
		
		//(三)在线索化右子树
		threadedNodes(node.getRight());
			
	}

线索化中序遍历

public void threadedList() {
		//定义一个变量,存储当前遍历的结点,从root开始
		HeroNode node = root;
		while(node != null) {
			//循环的找到leftType == 1的结点,第一个找到就是8结点
			//后面随着遍历而变化,因为当leftType==1时,说明该结点是按照线索化
			//处理后的有效结点
			while(node.getLeftType() == 0) {
				node = node.getLeft();
			}
			
			//打印当前这个结点
			System.out.println(node);
			//如果当前结点的右指针指向的是后继结点,就一直输出
			while(node.getRightType() == 1) {
				//获取到当前结点的后继结点
				node = node.getRight();
				System.out.println(node);
			}
			//替换这个遍历的结点
			node = node.getRight();
			
		}
	}

2 树的应用

2.1 堆排序⭐

  • 数组形式,顺序存储二叉式的应用
  • 大顶堆特点:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2] // i 对应第几个节点,i从0开始编号——>升序
  • 小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2] // i 对应第几个节点,i从0开始编号——>降序

堆排序的基本思想是:

  • 1.将待排序序列构造成一个大顶堆
  • 2.此时,整个序列的最大值就是堆顶的根节点。
  • 3.将其与末尾元素进行交换,此时末尾就为最大值。
  • 4.然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
    one:自底向上调整,所以只用调整两层!!!!!k = k * 2 + 1
    two:如果不break,就发生了交换,那么就得更新被交换的值(i=k)!!!

    three:时间复杂度nlogn
	//编写一个堆排序的方法,例子int arr[] = {4, 6, 8, 5, 9}
	public static void heapSort(int arr[]) {
		int temp = 0;
		//将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆
		for(int i = arr.length / 2 -1; i >=0; i--) {
			adjustHeap(arr, i, arr.length/);
		}
		//将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
  	// 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
		for(int j = arr.length-1;j >0; j--) {
			//交换
			temp = arr[j];
			arr[j] = arr[0];
			arr[0] = temp;
			adjustHeap(arr, 0, j); 
		}	
	}	
	//将一个数组(二叉树), 调整成一个大顶堆
	public  static void adjustHeap(int arr[], int i, int lenght) {		
		int temp = arr[i];										//先取出当前元素的值,保存在临时变量								
		for(int k = i * 2 + 1; k < lenght; k = k * 2 + 1) {		//1. k = i * 2 + 1 k 是 i结点的左子结点,																// k = k * 2 + 1,如果不break,就发生了交换,那么就得更新被交换的值(i=k)
			if(k+1 < lenght && arr[k] < arr[k+1]) { 			//说明左子结点的值小于右子结点的值
				k++; 											// k 指向右子结点
			}
			if(arr[k] > temp) { 								//如果子结点大于父结点
				arr[i] = arr[k]; 								//把较大的值赋给当前结点
				i = k; 											//!!! i 指向 k,继续循环比较
			} else {
				break;											//!两层敢于break
			}
		}										//当for 循环结束后,我们已经将以i 为父结点的树的最大值,放在了 最顶(局部)	
		arr[i] = temp;											//将temp值放到调整后的位置
	}

2.2 赫夫曼树

详情讲的太好了

2.3 赫夫曼编码

2.4 二叉排序树(BST⭐)

 根节点大于等于左子树所有节点,小于等于右子树所有节点。
 二叉查找树中序遍历有序。
 
1. 根节点的值大于其左子树中任意一个节点的值

2. 根结点的值小于其右节点中任意一节点的值

3. 这一规则适用于二叉查找树中的每一个节点。
2.4.1基本定义结点和树

结点类

//创建Node结点
class Node {
	int value;
	Node left;
	Node right;
	public Node(int value) {		
		this.value = value;
	}
	//查找要删除的结点
	/*
	 * @param value 希望删除的结点的值
	 * @return 如果找到返回该结点,否则返回null
	 */
	public Node search(int value) {
		if(value == this.value) { //找到就是该结点
			return this;
		} else if(value < this.value) {//如果查找的值小于当前结点,向左子树递归查找
			//如果左子结点为空
			if(this.left  == null) {
				return null;
			}
			return this.left.search(value);
		} else { //如果查找的值不小于当前结点,向右子树递归查找
			if(this.right == null) {
				return null;
			}
			return this.right.search(value);
		}		
	}
	//查找要删除结点的父结点
	/**	
	 * @param value 要找到的结点的值
	 * @return 返回的是要删除的结点的父结点,如果没有就返回null
	 */
	public Node searchParent(int value) {
		//如果当前结点就是要删除的结点的父结点,就返回
		if((this.left != null && this.left.value == value) || 
				(this.right != null && this.right.value == value)) {
			return this;
		} else {
			//如果查找的值小于当前结点的值, 并且当前结点的左子结点不为空
			if(value < this.value && this.left != null) {
				return this.left.searchParent(value); //向左子树递归查找
			} else if (value >= this.value && this.right != null) {
				return this.right.searchParent(value); //向右子树递归查找
			} else {
				return null; // 没有找到父结点
			}
		}		
	}
	
	@Override
	public String toString() {
		return "Node [value=" + value + "]";
	}
	//添加结点的方法
	//递归的形式添加结点,注意需要满足二叉排序树的要求
	//从上往下找添加结点的位子
	public void add(Node node) {
		if(node == null) {
			return;
		}		
		//判断传入的结点的值,和当前子树的根结点的值关系
		if(node.value < this.value) {
			//如果当前结点左子结点为null
			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();
		}
	}	
}

树类

//创建二叉排序树
class BinarySortTree {
	private Node root;	
	public Node getRoot() {
		return root;
	}
	//查找要删除的结点
	public Node search(int value) {
		if(root == null) {
			return null;
		} else {
			return root.search(value);
		}
	}	
	//查找父结点
	public Node searchParent(int value) {
		if(root == null) {
			return null;
		} else {
			return root.searchParent(value);
		}
	}	
	//编写方法: 
	//1. 返回的 以node 为根结点的二叉排序树的最小结点的值
	//2. 删除node 为根结点的二叉排序树的最小结点
	/*	
	 * @param node 传入的结点(当做二叉排序树的根结点)
	 * @return 返回的 以node 为根结点的二叉排序树的最小结点的值
	 */
	public int delRightTreeMin(Node node) {
		Node target = node;
		//循环的查找左子节点,就会找到最小值
		while(target.left != null) {
			target = target.left;
		}
		//这时 target就指向了最小结点
		//删除最小结点
		delNode(target.value);
		return target.value;
	}	
	//删除结点
	public void delNode(int value) {
		if(root == null) {
			return;
		}else {
			//1.需求先去找到要删除的结点  targetNode
			Node targetNode = search(value);
			//如果没有找到要删除的结点
			if(targetNode == null) {
				return;
			}
			//如果我们发现当前这颗二叉排序树只有一个结点
			if(root.left == null && root.right == null) {
				root = null;
				return;
			}			
			//去找到targetNode的父结点
			Node parent = searchParent(value);
			//如果要删除的结点是叶子结点
			if(targetNode.left == null && targetNode.right == null) {
				//判断targetNode 是父结点的左子结点,还是右子结点
				if(parent.left != null && parent.left.value == value) { //是左子结点
					parent.left = null;
				} else if (parent.right != null && parent.right.value == value) {//是由子结点
					parent.right = null;
				}
			} else if (targetNode.left != null && targetNode.right != null) { //删除有两颗子树的节点
				int minVal = delRightTreeMin(targetNode.right);
				targetNode.value = minVal;								
			} else { // 删除只有一颗子树的结点
				//如果要删除的结点有左子结点 
				if(targetNode.left != null) {
					if(parent != null) {
						//如果 targetNode 是 parent 的左子结点
						if(parent.left.value == value) {
							parent.left = targetNode.left;
						} else { //  targetNode 是 parent 的右子结点
							parent.right = targetNode.left;
						} 
					} else {
						root = targetNode.left;
					}
				} else { //如果要删除的结点有右子结点 
					if(parent != null) {
						//如果 targetNode 是 parent 的左子结点
						if(parent.left.value == value) {
							parent.left = targetNode.right;
						} else { //如果 targetNode 是 parent 的右子结点
							parent.right = targetNode.right;
						}
					} else {
						root = targetNode.right;
					}
				}				
			}			
		}
	}	
	//添加结点的方法
	public void add(Node node) {
		if(root == null) {
			root = node;//如果root为空则直接让root指向node
		} else {
			root.add(node);
		}
	}
	//中序遍历
	public void infixOrder() {
		if(root != null) {
			root.infixOrder();
		} else {
			System.out.println("二叉排序树为空,不能遍历");
		}
	}
}
2.4.2 往BST内插入结点

递归,写在结点类中

 	//添加结点的方法
	//递归的形式添加结点,注意需要满足二叉排序树的要求
	//从上往下找添加结点的位子
	public void add(Node node) {
		if(node == null) {
			return;
		}		
		//判断传入的结点的值,和当前子树的根结点的值关系
		if(node.value < this.value) {
			//如果当前结点左子结点为null
			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);
			}			
		}
	}
2.4.3 遍历以中序遍历为例,递归方法

写在结点类中

	//中序遍历
	public void infixOrder() {
		if(this.left != null) {
			this.left.infixOrder();
		}
		System.out.println(this);
		if(this.right != null) {
			this.right.infixOrder();
		}
	}
2.4.4 删除结点

思路:

2.4.1 查找结点

(删除前看是否存在)结点类中

//查找要删除的结点
	/**
	 * 
	 * @param value 希望删除的结点的值
	 * @return 如果找到返回该结点,否则返回null
	 */
	public Node search(int value) {
		if(value == this.value) { 			//找到就是该结点
			return this;
		} else if(value < this.value) {		//如果查找的值小于当前结点,向左子树递归查找
											//如果左子结点为空
			if(this.left  == null) {
				return null;
			}
			return this.left.search(value);
		} else { 							//如果查找的值不小于当前结点,向右子树递归查找
			if(this.right == null) {
				return null;
			}
			return this.right.search(value);
		}	
	}
2.4.2 查找要删除结点的父结点

结点类中

	//
	/**
	 * @param value 要找到的结点的值
	 * @return 返回的是要删除的结点的父结点,如果没有就返回null
	 */
	public Node searchParent(int value) {					//如果当前结点就是要删除的结点的父结点,就返回	
		if((this.left != null && this.left.value == value) || 
				(this.right != null && this.right.value == value)) {
			return this;
		} else {		  									//如果查找的值小于当前结点的值, 并且当前结点的左子结点不为空
			if(value < this.value && this.left != null) {
				return this.left.searchParent(value); 		//向左子树递归查找
			} else if (value >= this.value && this.right != null) {
				return this.right.searchParent(value); 		//向右子树递归查找
			} else {
				return null;								 // 没有找到父结点
			}
		}		
	}
2.4.3 删除结点

树类中

//删除结点
	public void delNode(int value) {
		if(root == null) {
			return;
		}else {
			//1.需求先去找到要删除的结点  targetNode
			Node targetNode = search(value);
			//如果没有找到要删除的结点
			if(targetNode == null) {
				return;
			}
			//如果我们发现当前这颗二叉排序树只有一个结点
			if(root.left == null && root.right == null) {
				root = null;
				return;
			}
			
			//去找到targetNode的父结点
			Node parent = searchParent(value);
			//如果要删除的结点是叶子结点
			if(targetNode.left == null && targetNode.right == null) {
				//判断targetNode 是父结点的左子结点,还是右子结点
				if(parent.left != null && parent.left.value == value) { //是左子结点
					parent.left = null;
				} else if (parent.right != null && parent.right.value == value) {//是由子结点
					parent.right = null;
				}
			} else if (targetNode.left != null && targetNode.right != null) { //删除有两颗子树的节点
				int minVal = delRightTreeMin(targetNode.right);
				targetNode.value = minVal;
				
				
			} else { // 删除只有一颗子树的结点
				//如果要删除的结点有左子结点 
				if(targetNode.left != null) {
					if(parent != null) {
						//如果 targetNode 是 parent 的左子结点
						if(parent.left.value == value) {
							parent.left = targetNode.left;
						} else { //  targetNode 是 parent 的右子结点
							parent.right = targetNode.left;
						} 
					} else {
						root = targetNode.left;
					}
				} else { //如果要删除的结点有右子结点 
					if(parent != null) {
						//如果 targetNode 是 parent 的左子结点
						if(parent.left.value == value) {
							parent.left = targetNode.right;
						} else { //如果 targetNode 是 parent 的右子结点
							parent.right = targetNode.right;
						}
					} else {
						root = targetNode.right;
					}
				}				
			}			
		}
	}

2.5 平衡二叉树(AVL)

BST-》AVL

1. 拥有BST树的特点:根节点的值大于其左子树中任意一个节点的值,小于其右节点中任意一节点的值,这一规则适用于二叉查找树中的每一个节点。

2. AVL树上任意结点的左、右子树的高度差最大为1。
  • 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。常用算法有红黑树、AVL、Treap、伸展树等。在平衡二叉搜索树中,我们可以看到,其高度一般都良好地维持在O(log2n),大大降低了操作的时间复杂度。

  • 调整平衡的基本思想:
    当在二叉排序树中插入一个节点时,首先检查是否因插入而破坏了平衡,若破坏,则找出其中的最小不平衡二叉树,在保持二叉排序树特性的情况下,调整最小不平衡子树中节点之间的关系,以达到新的平衡。
    所谓最小不平衡子树,指离插入节点最近且以平衡因子的绝对值大于1的节点作为根的子树。

先插入指定节点,记录下当前节点的信息,LH,EH或者RH。

  1. 若左子树高LH,查看其左子树根节点的信息,若是LH,则一次右旋;若是RH,则一次左旋+一次右旋
  2. 若右子树高RH,查看右子树根节点的信息,若是RH,则一次左旋;若是LH,则一次右旋+一次左旋
  3. 调整改变的节点信息

追求绝对的高度平衡,随着树的高度的增加,动态插入和删除的代价也随之增加

2.6 红黑树

BST->AVL->红黑树

  • 红黑树(Red Black Tree) 是一种自平衡二叉查找树
    红黑树和AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。
    二叉平衡树的严格平衡策略以牺牲建立查找结构(插入,删除操作)的代价,换来了稳定的O(logN) 的查找时间复杂度
    它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。
(1) 非黑即红。
(2) 首尾为黑色(头结点和叶子结点为黑)。
(3) 红父黑子。
(4) 路径黑色数量一致。

RBT 的操作代价分析:

  • (1) 查找代价:由于红黑树的性质(最长路径长度不超过最短路径长度的2倍),可以说明红黑树虽然不像AVL一样是严格平衡的,但平衡性能还是要比BST要好。其查找代价基本维持在O(logN)左右,但在最差情况下(最长路径是最短路径的2倍少1),比AVL要略逊色一点。
  • (2) 插入代价:RBT插入结点时,需要旋转操作和变色操作。但由于只需要保证RBT基本平衡就可以了。因此插入结点最多只需要2次旋转,这一点和AVL的插入操作一样。虽然变色操作需要O(logN),但是变色操作十分简单,代价很小。
  • (3) 删除代价:RBT的删除操作代价要比AVL要好的多,删除一个结点最多只需要3次旋转操作。
    RBT 效率总结 : 查找 效率最好情况下时间复杂度为O(logN),但在最坏情况下比AVL要差一些,但也远远好于BST。
    插入和删除操作改变树的平衡性的概率要远远小于AVL(RBT不是高度平衡的)。因此需要的旋转操作的可能性要小,而且一旦需要旋转,插入一个结点最多只需要旋转2次,删除最多只需要旋转3次(小于AVL的删除操作所需要的旋转次数)。虽然变色操作的时间复杂度在O(logN),但是实际上,这种操作由于简单所需要的代价很小。

红黑树能够以O(log2(N))的时间复杂度进行搜索、插入、删除操作。此外,任何不平衡都会在3次旋转之内解决。这一点是AVL所不具备的。

3.多路查找树

3.1 二叉树与B树

3.2 2-3树

3.3 B数、B+树、B*树

链接
B树:

B树的阶数:M阶表示 一个B树的结最多有多少个查找路径(即这个结点有多少个子节点)。M=M路,M=2是二叉树,M=3则是三叉树。

一棵M阶B树有以下特点。

特点:

1.   每个结点的值(索引) 都是按递增次序排列存放的,并遵循左小右大原则。

2.  根结点 的 子节点 个数为 [2,M]3. 除 根结点 以外 的 非叶子结点 的子节点个数 为[ Math.ceil(M/2),M]。 Math.ceil() 为向上取整。

4. 每个 非叶子结点 的值(索引) 个数 = 子节点个数 -1 。最小为 Math.ceil(M/2)-1   最大为 M-1 个。

5. B树的所有叶子结点都位于同一层。

B+树:

1. B+树内部有两种结点,一种是索引结点,一种是叶子结点。

2. B+树的索引结点并不会保存记录,只用于索引,所有的数据都保存在B+树的叶子结点中。而B树则是所有结点都会保存数据。

3. B+树的叶子结点都会被连成一条链表。叶子本身按索引值的大小从小到大进行排序。即这条链表是 从小到大的。多了条链表方便范围查找数据。

4. B树的所有索引值是不会重复的,而B+树 非叶子结点的索引值 最终一定会全部出现在 叶子结点中。

4 关于dfs和bfs

dfs常用

void dfs(TreeNode root) {
    if (root == null) {
        return;
    }
    dfs(root.left);
    dfs(root.right);
}

bfs:层序遍历、最短路径问题(也可使用Dijkstra 算法)102、1062

void bfs(TreeNode root) {
    Queue<TreeNode> queue = new ArrayDeque<>();
    queue.add(root);
    while (!queue.isEmpty()) {
        TreeNode node = queue.poll(); // Java 的 pop 写作 poll()
        if (node.left != null) {
            queue.add(node.left);
        }
        if (node.right != null) {
            queue.add(node.right);
        }
    }
}

网格dfs

bfs讲解

bfs和dfs对比:https://leetcode-cn.com/problems/binary-tree-level-order-traversal/solution/bfs-de-shi-yong-chang-jing-zong-jie-ceng-xu-bian-l/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

zkFun

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

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

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

打赏作者

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

抵扣说明:

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

余额充值