数据结构(六)查找算法、哈希表、二叉树

查找算法

  1. 顺序(线性)查找
  2. 二分查找/折半查找
  3. 插值查找
  4. 斐波那契查找

顺序(线性)查找

有一个数列: {1,8, 10, 89, 1000, 1234},判断数列中是否包含此名称【顺序查找】 要求: 如果找到了,就提示找到,并给出下标值。
代码实现:

package com.jxust.search;

public class SeqSearch {

	public static void main(String[] args) {
		int arr[] = {1,9,11,-1,34,89};
		int index = seqSearch(arr, 11);
		if(index == -1) {
			System.out.println("没有找到");
		}else {
			System.out.println("找到了,下标为="+index);
		}
	}
	
	//这里我们实现的线性查找是找到一个满足条件的值,就返回
	public static int seqSearch(int[] arr,int value) {
		//线性查找是逐一比对,发现有相同值,就返回下标
		for(int i=0;i<arr.length;i++) {
			if(arr[i] == value) {
				return i;
			}
		}
		return -1;
	}
}

二分查找

请对一个有序数组进行二分查找 {1,8, 10, 89, 1000, 1234},输入一个数看看该数组是否存在此数,并且求出下标,如果没有就提示"没有这个数"。

思路分析:

  1. 首先确定该数组的中间的下标
    mid = (left + right) / 2
  2. 然后让需要查找的数 findVal 和 arr[mid] 比较
    findVal > arr[mid], 说明你要查找的数在mid 的右边, 因此需要递归的向右查找
    findVal < arr[mid], 说明你要查找的数在mid 的左边, 因此需要递归的向左查找
    findVal == arr[mid]说明找到,就返回

什么时候我们需要结束递归?
①找到就结束递归
②递归完整个数组,仍然没有找到findVal ,也需要结束递归 。当left > right就需要退出

代码实现:

package com.jxust.search;
//注意:使用二分查找的前提是该数组是有序的
public class BinarySearch {
	
	public static void main(String[] args) {
		int arr[] = {1,8,10,89,1000,1234};
		int resIndex = binarySearch(arr, 0, arr.length -1, 89);
		if(resIndex == -1) {
			System.out.println("没有这个数");
		}else {
			System.out.println("resIndex = "+resIndex);
		}
	}
	//二分查找算法
	/**
	 * 
	 * @param arr 数组
	 * @param left  左边的索引
	 * @param right 右边的索引
	 * @param findVal 要查找的值
	 * @return  如果找到就返回下标,如果没有找到,就返回-1
	 */
	public static int binarySearch(int[] arr,int left,int right,int findVal) {
		//当left>right 时,说明递归整个数组但没有找到
		if(left>right) {
			return -1;
		}
		int mid = (left+right)/2;
		int midVal = arr[mid];
		if(findVal > midVal) {//向右递归
			return binarySearch(arr, mid+1, right, findVal);
		}else if(findVal < midVal) { //向左递归
			return binarySearch(arr, left, mid-1, findVal);
		}else {
			return mid;
		}
	}
}

优化、完善:
{1,8, 10, 89, 1000, 1000,1234} 当一个有序数组中,有多个相同的数值时,如何将所有的数值都查找到,比如这里的 1000。

思路分析:

  1. 在找到mid索引值后,不要马上返回
  2. 向mid索引值的左边扫描,将所有满足1000的元素的下标加入到集合ArrayList中
  3. 向mid索引值的右边扫描,将所有满足1000的元素的下标加入到集合ArrayList中
  4. 将ArrayList返回

代码实现:

package com.jxust.search;

import java.util.ArrayList;
import java.util.List;

//注意:使用二分查找的前提是该数组是有序的
public class BinarySearch {
	
	public static void main(String[] args) {
		int arr[] = {1,8,10,89,1000,1000,1000,1000,1234};
		List<Integer> resIndexList = binarySearch2(arr, 0, arr.length -1, 1000);
		System.out.println("resIndexList = "+resIndexList);
	}
	//有多个相同数值时,如何将所有的数值都找到
	public static List<Integer> binarySearch2(int[] arr,int left,int right,int findVal) {
		//当left>right 时,说明递归整个数组但没有找到
		if(left>right) {
			return new ArrayList<Integer>();
		}
		int mid = (left+right)/2;
		int midVal = arr[mid];
		if(findVal > midVal) {//向右递归
			return binarySearch2(arr, mid+1, right, findVal);
		}else if(findVal < midVal) { //向左递归
			return binarySearch2(arr, left, mid-1, findVal);
		}else {
			// 1、在找到mid索引值后,不要马上返回
			List<Integer> resIndexlist = new ArrayList<Integer>();
			//2、向mid索引值的左边扫描,将所有满足1000的元素的下标加入到集合ArrayList中
			int temp = mid-1;
			while(true) {
				if(temp<0 || arr[temp]!= findVal) { //退出
					break;
				}
				//否则,就把temp放入到resIndexlist中
				resIndexlist.add(temp);
				temp -= 1; //temp左移
			}
			resIndexlist.add(mid);
			//3、向mid索引值的右边扫描,将所有满足1000的元素的下标加入到集合ArrayList中
			temp = mid+1;
			while(true) {
				if(temp>arr.length-1 || arr[temp]!= findVal) { //退出
					break;
				}
				//否则,就把temp放入到resIndexlist中
				resIndexlist.add(temp);
				temp += 1; //temp左移
			}
			return resIndexlist;
		}
	}
}

插值查找

原理介绍: 插值查找算法也要求数组是有序的
插值查找算法类似于二分查找,不同的是插值查找每次从自适应mid处开始查找。
将折半查找中的求mid 索引的公式 :int mid = left + (right – left) * (findVal – arr[left]) / (arr[right] – arr[left])

注意

  1. 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找, 速度较快.
  2. 关键字分布不均匀的情况下,该方法不一定比折半查找要好

代码实现:

package com.jxust.search;

import java.util.Arrays;

public class InsertValueSearch {

	public static void main(String[] args) {
		int[] arr = new int[100];
		for(int i=0;i<100;i++) {
			arr[i] = i+1;
		}
		int index = insertValueSearch(arr, 0, arr.length-1, 100);
		System.out.println("index = "+index);
//		System.out.println(Arrays.toString(arr));
	}
	
	//插值查找算法
	//插值查找算法也要求数组是有序的
	//如果找到了就返回下标,如果没有找到就返回-1
	public static int insertValueSearch(int[] arr,int left,int right,int findVal)
	{
		//注意: findVal < arr[0] || findVal > arr[arr.length - 1]是必要的,否则我们得到的mid可能越界
		if(left > right || findVal < arr[0] || findVal > arr[arr.length - 1]) {
			return -1;
		}
		//求出mid
		int mid = left + (right - left) * (findVal - arr[left]) / (arr[right] - arr[left]);

		int midVal = arr[mid];
		if(findVal > midVal) { //说明要向右递归
			return insertValueSearch(arr, mid+1, right, findVal);
		}else if(findVal < midVal) { //说明要向左递归
			return insertValueSearch(arr, left, mid-1, findVal);
		}else {
			return mid;
		}
	}
	
}

斐波那契(黄金分割法)查找算法

原理:
斐波那契查找原理与前两种相似,仅仅改变了中间结点(mid)的位置,mid不再是中间或插值得到,而是位于黄金分割点附近,即mid=low+F(k-1)-1(F代表斐波那契数列)
在这里插入图片描述
F(k-1)-1的理解:
①由斐波那契数列 F[k]=F[k-1]+F[k-2] 的性质,可以得到 (F[k]-1)=(F[k-1]-1)+(F[k-2]-1)+1 。该式说明:只要顺序表的长度为F[k]-1,则可以将该表分成长度为F[k-1]-1和F[k-2]-1的两段,即如上图所示。从而中间位置为mid=low+F(k-1)-1

②类似的,每一子段也可以用相同的方式分割
③但顺序表长度n不一定刚好等于F[k]-1,所以需要将原来的顺序表长度n增加至F[k]-1。这里的k值只要能使得F[k]-1恰好大于或等于n即可,由以下代码得到,顺序表长度增加后,新增的位置(从n+1到F[k]-1位置),都赋为n位置的值即可。

while(n>fib(k)-1)
    k++;

代码实现:

package com.jxust.search;

import java.util.Arrays;

public class FibonacciSearch {

	public static int maxSize = 20;
	public static void main(String[] args) {
		int [] arr = {1,8, 10, 89, 1000, 1234};
		
		System.out.println("index=" + fibSearch(arr, 1));// 0
		
	}

	//因为后面我们mid=low+F(k-1)-1,需要使用到斐波那契数列,因此我们需要先获取到一个斐波那契数列
	//非递归方法得到一个斐波那契数列
	public static int[] fib() {
		int[] f = new int[maxSize];
		f[0] = 1;
		f[1] = 1;
		for (int i = 2; i < maxSize; i++) {
			f[i] = f[i - 1] + f[i - 2];
		}
		return f;
	}
	
	//编写斐波那契查找算法
	//使用非递归的方式编写算法
	/**
	 * 
	 * @param a  数组
	 * @param key 我们需要查找的关键码(值)
	 * @return 返回对应的下标,如果没有-1
	 */
	public static int fibSearch(int[] a, int key) {
		int low = 0;
		int high = a.length - 1;
		int k = 0; //表示斐波那契分割数值的下标
		int mid = 0; 
		int f[] = fib(); //获取到斐波那契数列
		//获取到斐波那契分割数值的下标
		while(high > f[k] - 1) {
			k++;
		}
		//因为 f[k] 值 可能大于 a 的 长度,因此我们需要使用Arrays类,构造一个新的数组,并指向temp[]
		//不足的部分会使用0填充
		int[] temp = Arrays.copyOf(a, f[k]);
		//实际上需求使用a数组最后的数填充 temp
		//temp = {1,8, 10, 89, 1000, 1234, 0, 0}  => {1,8, 10, 89, 1000, 1234, 1234, 1234}
		for(int i = high + 1; i < temp.length; i++) {
			temp[i] = a[high];
		}
		
		// 使用while来循环处理,找到我们的数 key
		while (low <= high) { 
			mid = low + f[k - 1] - 1;
			if(key < temp[mid]) { //我们应该继续向左查找
				high = mid - 1;
				//为什么是 k--
				/*1. 全部元素 = 前面的元素 + 后边元素
				2. f[k] = f[k-1] + f[k-2]
				因为 前面有 f[k-1]个元素,所以可以继续拆分 f[k-1] = f[k-2] + f[k-3]
				即 在 f[k-1] 的前面继续查找 k--
				即下次循环 mid = f[k-1-1]-1
				*/
				k--;
			} else if ( key > temp[mid]) { // 我们应该继续向右查找
				low = mid + 1;
				//为什么是k -=2
				//说明
				//1. 全部元素 = 前面的元素 + 后边元素
				//2. f[k] = f[k-1] + f[k-2]
				//3. 因为后面我们有f[k-2] 所以可以继续拆分 f[k-2] = f[k-3] + f[k-4]
				//4. 即在f[k-2] 的前面进行查找 k -=2
				//5. 即下次循环 mid = f[k - 1 - 2] - 1
				k -= 2;
			} else { 
				//需要确定,返回的是哪个下标
				if(mid <= high) {
					return mid;
				} else {
					return high;
				}
			}
		}
		return -1;
	}
}

哈希表

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
在这里插入图片描述
示例:
有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id,性别,年龄,住址…),当输入该员工的id时,要求查找到该员工的 所有信息。
思路分析:
在这里插入图片描述

代码实现:

package com.jxust.hashTab;

import java.util.Scanner;

public class HashTabDemo {

	public static void main(String[] args) {
		//创建哈希表
		HashTab hashTab = new HashTab(7);
		//菜单
		String key = "";
		Scanner scanner = new Scanner(System.in);
		while(true) {
			System.out.println("add:添加雇员");
			System.out.println("list:显示雇员");
			System.out.println("find:查找雇员");
			System.out.println("exit:退出系统");
			
			key = scanner.next();
			switch (key) {
			case "add":
				System.out.println("输入id");
				int id = scanner.nextInt();
				System.out.println("输入姓名");
				String name = scanner.next();
				//创建雇员
				Emp emp = new Emp(id,name);
				hashTab.add(emp);
				break;
			case "list":
				hashTab.list();
				break;
			case "exit":
				scanner.close();
				System.exit(0);
			case "find":
				System.out.println("请输入要查找的id");
				id = scanner.nextInt();
				hashTab.findEmpById(id);
				break;
			default:
				break;
			}
		}
	}

}
//表示一个雇员
class Emp{
	public int id;
	public String name;
	public Emp next;
	public Emp(int id,String name) {
		super();
		this.id = id;
		this.name = name;
	}
}

//创建HashTab管理多条链表
class HashTab{
	private EmpLinkedList[] empLinkedListArray;
	private int size; //表示共有多少条链表
	//构造器
	public HashTab(int size) {
		this.size = size;
		//初始化empLinkedListArray
		empLinkedListArray = new EmpLinkedList[size];
		//?,这是分别初始化每个链表
		for(int i=0;i<size;i++) {
			empLinkedListArray[i] = new EmpLinkedList();
		}
	}
	//添加雇员
	public void add(Emp emp) {
		//根据员工的id,得到该员工应当添加到哪条链表
		int empLinkedListNO = hashFun(emp.id);
		//将emp添加到对应的链表中
		empLinkedListArray[empLinkedListNO].add(emp);
	}
	
	 //遍历所有链表,遍历hashTab
	public void list() {
		for(int i=0;i<size;i++) {
			empLinkedListArray[i].list(i);
		}
	}
	
	//根据输入的id查找雇员
	public void findEmpById(int id) {
		//使用散列函数确定到哪条链表查找
		int empLinkedListNO = hashFun(id);
		Emp emp = empLinkedListArray[empLinkedListNO].findEmpById(id);
		if(emp != null) { //找到
			System.out.printf("在第%d条链表中找到雇员id = %d\n",(empLinkedListNO+1),id);
		}else {
			System.out.println("在哈希表中,没有找到该雇员");
		}
	}
	
	
	//编写散列函数,使用取模法
	public int hashFun(int id) {
		return id%size;
	}
}


//创建EmpLinkedList,表示链表
class EmpLinkedList{
	//头指针,指向第一个Emp
	private Emp head; //默认为null
	//添加雇员到链表
	//假定,当添加雇员时,id是自增长,即id的分配总是从小到大的
	//因此我们将该雇员直接加入到本链表的最后即可
	public void add(Emp emp) {
		//如果是添加第一个雇员
		if(head == null) {
			head = emp;
			return;
		}
		//如果不是第一个雇员直接加入到本链表的最后即可
		Emp curEmp = head;
		while(true) {
			if(curEmp.next == null) { //说明到链表最后
				break;
			}
			curEmp = curEmp.next;//后移
		}
		//退出时直接将emp加入链表
		curEmp.next = emp;
	}
	//遍历链表的雇员信息
	public void list(int no) {
		if(head == null) { //说明链表为空
			System.out.println("第 "+(no+1)+" 链表为空");
			return;
		}
		System.out.print("第 "+(no+1)+" 链表信息为:");
		Emp curEmp = head; //辅助指针
		while(true) {
			System.out.printf("=> id=%d name=%s\t",curEmp.id,curEmp.name);
			if(curEmp.next == null) {//说明curEmp已经是最后结点
				break;
			}
			curEmp = curEmp.next; //后移,遍历
		}
		System.out.println();
	}
	
	//根据id查找雇员
	//如果找到,就返回Emp,如果没有找到,就返回null
	public Emp findEmpById(int id) {
		//判断链表是否为空
		if(head == null) {
			System.out.println("链表为空");
			return null;
		}
		//辅助指针
		Emp curEmp = head;
		while(true) {
			if(curEmp.id == id) { //找到
				break; //这时curEmp就只想要查找的雇员
			}
			//退出
			if(curEmp.next == null) { //说明遍历当前链表并没有找到该雇员
				curEmp = null;
				break;
			}
			curEmp = curEmp.next;
		}
		return curEmp;
	}
}

二叉树

为什么需要树这种数据结构

  1. 数组存储方式的分析
    优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。
    缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低

  2. 链式存储方式的分析
    优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可, 删除效率也很好)。
    缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历)

  3. 树存储方式的分析
    能提高数据存储,读取的效率, 比如利用 二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。

树的示意图

在这里插入图片描述

二叉树的概念

  1. 树有很多种,每个节点最多只能有两个子节点的一种形式称为二叉树
  2. 二叉树的子节点分为左节点和右节点。
  3. 如果该二叉树的所有叶子节点都在最后一层,并且结点总数= 2^n -1 , n 为层数,则我们称为满二叉树
  4. 如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树
    在这里插入图片描述

前序、后序、中序遍历

前序遍历: 先输出父节点,再遍历左子树和右子树
中序遍历: 先遍历左子树,再输出父节点,再遍历右子树
后序遍历: 先遍历左子树,再遍历右子树,最后输出父节点

小结: 看输出父节点的顺序,就确定是前序,中序还是后序

使用前序,中序和后序对下面的二叉树进行遍历:
在这里插入图片描述
**思路分析:**二叉树前序、中序、后序遍历步骤

  1. 创建一棵二叉树
  2. 前序遍历
    ①先输出当前节点(初始的时候是root)
    ②如果左子节点不为空,则递归前序遍历
    ③如果右子节点不为空,则递归前序遍历
  3. 中序遍历
    ①如果当前节点的左子节点不为空,则递归中序遍历
    ②输出当前节点
    ③如果当前节点的右子节点不为空,则递归中序遍历
  4. 后序遍历
    ①如果当前节点的左子节点不为空,则递归后序遍历
    ②如果当前节点的右子节点不为空,则递归后序遍历
    ③输出当前节点

代码实现:

package com.jxust.tree;

public class BinaryTreeDemo {

	public static void main(String[] args) {
		//现需要创建一棵二叉树
		BinaryTree binaryTree = new BinaryTree();
		//创建需要的结点
		HeroNode root = new HeroNode(1,"宋江");
		HeroNode node2 = new HeroNode(2,"吴用");
		HeroNode node3 = new HeroNode(3,"卢俊义");
		HeroNode node4 = new HeroNode(4,"林冲");
		HeroNode node5 = new HeroNode(5,"关胜");
		
		root.setLeft(node2);
		root.setRight(node3);
		node3.setRight(node4);
		node3.setLeft(node5);
		binaryTree.setRoot(root);
		
		System.out.println("前序遍历");
		binaryTree.preOrder();
		System.out.println("中序遍历");
		binaryTree.infixOrder();
		System.out.println("后序遍历");
		binaryTree.postOrder();
	}
	
}

//定义一个二叉树
class BinaryTree {
	private HeroNode root;

	public void setRoot(HeroNode root) {
		this.root = root;
	}

	// 前序遍历
	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("二叉树为空,无法遍历!");
		}
	}

}


//先创建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 + "]";
	}
	
	//前序遍历的方法
	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. 前序查找:
    ①先判断当前结点的no是否等于要查找的no
    ②如果相等,则返回当前结点
    ③如果不等,则判断当前结点的左子节点是否为空,不过不为空则递归前序查找
    ④如果左递归前序查找,找到结点,则返回,否则继续判断,当前结点的右子节点是否为空,如果不为空,则继续向右递归前序查找
  2. 中序查找
    ①判断当前结点的左子节点是否为空,如果不为空,则递归中序查找
    ②如果找到则返回,如果没有找到,就和当前结点比较,如果相等就返回当前结点,否则继续向右递归中序查找
    ③如果有递归中序查找,找到就返回,否则返回null
  3. 后序查找
    ①判断当前结点的左子节点是否为空,如果不为空,则递归后序查找
    ②如果找到则返回,如果没有找到,判断当前结点的右子节点是否为空,如果不为空,则有递归进行后序查找,如果找到就返回
    ③和当前结点比较,如果相等返回,否则返回null

代码实现:

package com.jxust.tree;

public class BinaryTreeDemo {

	public static void main(String[] args) {
		// 现需要创建一棵二叉树
		BinaryTree binaryTree = new BinaryTree();
		// 创建需要的结点
		HeroNode root = new HeroNode(1, "宋江");
		HeroNode node2 = new HeroNode(2, "吴用");
		HeroNode node3 = new HeroNode(3, "卢俊义");
		HeroNode node4 = new HeroNode(4, "林冲");
		HeroNode node5 = new HeroNode(5, "关胜");

		root.setLeft(node2);
		root.setRight(node3);
		node3.setRight(node4);
		node3.setLeft(node5);
		binaryTree.setRoot(root);

		//前序遍历查找
		System.out.println("前序遍历查找");
		HeroNode resNode = binaryTree.preOrderSearch(5);
		if(resNode != null) {
			System.out.printf("找到了,信息为no=%d name=%s",resNode.getNo(),resNode.getName());
		}else {
			System.out.printf("没有找到no=%d的英雄",5);
		}
		
	}

}

// 定义一个二叉树
class BinaryTree {
	private HeroNode root;

	public void setRoot(HeroNode root) {
		this.root = root;
	}

	// 前序遍历查找
	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 root.postOrderSearch(no);
		} else {
			return null;
		}
	}

}

// 先创建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 + "]";
	}

	// 前序遍历查找
	public HeroNode preOrderSearch(int no) {
		// 比较当前结点是不是
		if (this.no == no) {
			return this;
		}
		// 判断左子节点是否为空,如果不为空则递归前序遍历
		HeroNode resNode = null;
		if (this.left != null) {
			resNode = this.left.preOrderSearch(no);
		}
		if (resNode != null) { // 说明左子树找到了
			return resNode;
		}
		// 判断右子节点是否为空,如果不为空则递归前序遍历
		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;
		}
		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;
		}
		// 如果左右子树都没找到,就比较当前结点
		if (this.no == no) {
			return this;
		}
		return resNode;
	}
}

二叉树-删除节点

要求
①如果删除的节点是叶子节点,则删除该节点
②如果删除的节点是非叶子节点,则删除该子树
在这里插入图片描述
思路分析:
如果树是空树root,如果只有一个root结点,则等价将二叉树置空

  1. 二叉树是单向的,所以我们是判断当前结点的子结点是否需要删除
  2. 如果当前结点的左子结点不为空,并且左子结点就是要删除的结点,就将this.left = null,并且返回(结束递归删除)
  3. 如果当前结点的右子结点不为空,并且右子结点就是要删除的结点,就将this.right = null,并且返回(结束递归删除)
  4. 如果第2步和第3步都没有删除结点,那我们就需要对左子树进行递归删除
  5. 如果第4步也没有删除结点,则应当向右子树进行递归删除

代码实现:

package com.jxust.tree;

public class BinaryTreeDemo {

	public static void main(String[] args) {
		// 现需要创建一棵二叉树
		BinaryTree binaryTree = new BinaryTree();
		// 创建需要的结点
		HeroNode root = new HeroNode(1, "宋江");
		HeroNode node2 = new HeroNode(2, "吴用");
		HeroNode node3 = new HeroNode(3, "卢俊义");
		HeroNode node4 = new HeroNode(4, "林冲");
		HeroNode node5 = new HeroNode(5, "关胜");

		root.setLeft(node2);
		root.setRight(node3);
		node3.setRight(node4);
		node3.setLeft(node5);
		binaryTree.setRoot(root);

		System.out.println("删除前,前序遍历");
		binaryTree.preOrder();
		binaryTree.delNode(5);
		System.out.println("删除后,前序遍历");
		binaryTree.preOrder();
	}

}

// 定义一个二叉树
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("二叉树为空,无法遍历!");
		}
	}

}

// 先创建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 + "]";
	}

	// 递归删除结点
	// 如果删除的节点是叶子节点,则删除该节点
	// 如果删除的节点是非叶子节点,则删除该子树.
	public void delNode(int no) {
		if (this.left != null && this.left.no == no) {
			this.left = null;
			return;
		}
		if (this.right != null && this.right.no == no) {
			this.right = null;
			return;
		}
		// 向左子树进行递归删除
		if (this.left != null) {
			this.left.delNode(no);
		}
		// 向右子树进行递归删除
		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();
		}
	}	
}

顺序存储二叉树

从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组。
在这里插入图片描述
要求:
①要求以数组的方式来存放 arr : [1, 2, 3, 4, 5, 6, 6]
②要求在遍历数组 arr时,仍然可以以前序遍历,中序遍历和后序遍历的方式完成结点的遍历

顺序存储二叉树的特点:

①顺序二叉树通常只考虑完全二叉树
②第n个元素的左子节点为 2 * n + 1
③第n个元素的右子节点为 2 * n + 2
④第n个元素的父节点为 (n-1) / 2
⑤n : 表示二叉树中的第几个元素(按0开始编号)

示例:需求: 给你一个数组 {1,2,3,4,5,6,7},要求以二叉树前序遍历的方式进行遍历。 前序遍历的结果应当为 1,2,4,5,3,6,7

代码实现:

package com.jxust.tree;

public class ArrBinaryTree {

	public static void main(String[] args) {
		int[] arr = {1,2,3,4,5,6,7};
		//创建一个ArrayBinaryTree
		ArrayBinaryTree arrayBinaryTree = new ArrayBinaryTree(arr);
		arrayBinaryTree.preOrder();
	}

}

//编写一个ArrayBinaryTree,实现顺序存储二叉树遍历
class ArrayBinaryTree{
	private int[] arr; //存储数据结点的数组
	public ArrayBinaryTree(int[] arr) {
		this.arr = arr;
	}
	
	//重载preOrder
	public void preOrder() {
		this.preOrder(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(index*2+2);
		}
	}
}

线索化二叉树

线索二叉树基本介绍

① n个结点的二叉链表中含有 n+1 【公式 2n-(n-1)=n+1】 个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")

② 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种

③一个结点的前一个结点,称为前驱结点
④一个结点的后一个结点,称为后继结点

示例:
思路分析: 中序遍历的结果:{8, 3, 10, 1, 14, 6}
在这里插入图片描述
说明: 当线索化二叉树后,Node节点的属性 left 和 right ,有如下情况:

  1. left 指向的是左子树,也可能是指向的前驱节点. 比如 ① 节点 left 指向的左子树, 而 ⑩ 节点的 left 指向的就是前驱节点.
  2. right指向的是右子树,也可能是指向后继节点,比如 ① 节点right 指向的是右子树,而⑩ 节点的right 指向的是后继节点.

代码实现:

package threadedbinarytree;

public class ThreadedBinaryTreeDemo {

	public static void main(String[] args) {
		HeroNode root = new HeroNode(1, "Tom");
		HeroNode node2 = new HeroNode(3, "jack");
		HeroNode node3 = new HeroNode(6, "smith");
		HeroNode node4 = new HeroNode(8, "mary");
		HeroNode node5 = new HeroNode(10, "king");
		HeroNode node6 = new HeroNode(14, "dim");

		root.setLeft(node2);
		root.setRight(node3);
		node2.setLeft(node4);
		node2.setRight(node5);
		node3.setLeft(node6);
		//中序线索化
		ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
		threadedBinaryTree.setRoot(root);
		threadedBinaryTree.threadedNodes();
		//测试,以10号结点为例
		HeroNode leftNode = node5.getLeft();
		HeroNode rightNode = node5.getRight();
		System.out.println("10号结点的前驱结点是"+leftNode);
		System.out.println("10号结点的后继结点是"+rightNode);
	
		System.out.println("使用线索化的方式遍历线索化二叉树");
		threadedBinaryTree.threadedList();
	}

}

//定义一个二叉树
class ThreadedBinaryTree {
	private HeroNode root;
	//为了实现线索化,需要创建当前结点的前驱结点的指针
	//在递归进行线索化时,pre总是保留前一个结点
	private HeroNode pre = null;

	public void setRoot(HeroNode root) {
		this.root = root;
	}
	
	//重载threadedNodes方法
	public void threadedNodes() {
		this.threadedNodes(root);
		
	}
	
	//遍历中序线索二叉树
	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();
		}
		
	}
	
	//编写对二叉树进行中序线索化方法
	/**
	 * 
	 * @param node 当前需要线索化的结点
	 */
	public void threadedNodes(HeroNode node) {
		//如果node == null,不能线索化
		if(node == null) {
			return;
		}
		//1.先线索化左子树
		threadedNodes(node.getLeft());
		//2.线索化当前结点
		
		//处理当前结点的前驱结点
		if(node.getLeft() == null) {
			//让当前结点的左指针指向前驱结点
			node.setLeft(pre);
			//修改当前结点的左指针
			node.setLeftType(1);
		}
		//处理后继结点
		if(pre != null && pre.getRight() == null) {
			//让前驱结点的右指针指向当前结点
			pre.setRight(node);
			//修改前驱结点的右指针类型
			pre.setRightType(1);
		}
		//每处理一个结点后,让当前结点是下一个结点的前驱结点
		pre = node;
		//3.线索化右子树
		threadedNodes(node.getRight());
	}
	
}



//创建HeroNode结点
class HeroNode {
	private int no;
	private String name;
	private HeroNode left; // 默认为null
	private HeroNode right; // 默认为null
	/*说明:
	 * 1.如果leftType == 0 表示指向的是左子树,如果是1则表示指向的是前驱结点
	 * 2.如果rightType == 0 表示指向的是右子树,如果是1则表示指向的是后继结点
	 */
	private int leftType;
	private int rightType;
	
	public HeroNode(int no, String name) {
		this.no = no;
		this.name = name;
	}
	
	public int getLeftType() {
		return leftType;
	}

	public void setLeftType(int leftType) {
		this.leftType = leftType;
	}

	public int getRightType() {
		return rightType;
	}

	public void setRightType(int rightType) {
		this.rightType = rightType;
	}

	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 + "]";
	}

}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值