目录
本文是参考牛客网的题解结合自己理解写的,题解:牛客题解
其中题解采用C++,博主已入JAVA坑,理解官方题解意思,使用JAVA写的,并自己输入数据测试,可以观察运行结果,理解程序逻辑
题目描述
给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。
题目理解
博主重新复习了二叉树的一些概念,把二叉树做了一些梳理,首先二叉搜索树的定义是什么?只有理解了它,我们才能理解为什么使用中序遍历.首先看二叉搜索树的定义:
二叉搜索树,是指一棵空树或者具有下列性质的二叉树:
1.若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
2.若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
3.任意节点的左,右子树也分别为二叉搜索树;
4.没有键值相等的节点。
总结就是左子节点键值小于根节点的键值,根节点的键值小于右子节点的键值.
二叉树的前序,中序,后序遍历
二叉树由根结点及左、右子树这三个基本部分组成, 对应每个节点有:
⑴访问结点本身(N)
⑵遍历该结点的左子树(L)
⑶遍历该结点的右子树(R)
根据访问根节点的先后顺序我们定义前序,中序,后序遍历.前序遍历是首先访问根节点,再访问左子节点,再访问右子节点(NLR),相应的中序遍历就是LNR,后序遍历就是LRN.
从搜索二叉树和中序遍历的定义来看,我们可以看出按照中序遍历的方式访问搜索二叉树输出的是一个递增序列,可以观察我下面程序运行结果.这样我们查找第k小的节点就很容易了
顺序存储二叉树的概念
以如下图所示的二叉树为例,我们以数组{5,3,7,2,4,6,8}的方式存储二叉树(这里一般只考虑完全二叉树),那么数组下标和根节点以及它的左右子节点有什么关系呢?
观察上图可以发现根节点的键值为5,它的左子树的键值为3,数组下标为1,它的右子树下标2,那么就有这样的关系:
第n个元素的左子节点为2*n+1;
第n个元素的右子节点为2*n+2;
第n个元素的父节点为(n-1)/2;
n:表示二叉树中第几个元素(按0开始编号)
根据下面的程序大家可以观察相应的前中后序遍历输出结果
package JZ62_search_tree;
/*
* 用二叉链表做为存储结构,中序遍历算法可描述为:
* void InOrder(BinTree T)
* {
* if(T)
* { // 如果二叉树非空
* InOrder(T->lchild);
* printf("%c",T->data); // 访问结点
* InOrder(T->rchild);
* }
* } // InOrde
*/
class ArrBinaryTree{
private int[] arr;//存储数据结构的数组
public ArrBinaryTree(int[] arr) {
this.arr=arr;
}
public void preOrder() {
this.preOrder(0);
}
public void infixOrder() {
this.infixOrder(0);
}
public void postOrder() {
this.postOrder(0);
}
//编写一个方法,完成顺序存储二叉树的前序遍历
public void preOrder(int index) {
//如果数组为空
if(arr.length==0) {
System.out.println("数组为空");
}
System.out.println(arr[index]);
//向左递归遍历
if(2*index+1<arr.length)
{
preOrder(2*index+1);
}
//向右递归遍历
if(2*index+2<arr.length) {
preOrder(2*index+2);
}
}
//中序遍历
public void infixOrder(int index) {
if(arr.length==0) {
System.out.println("数组为空");
}
//向左递归遍历
if(2*index+1<arr.length)
{
infixOrder(2*index+1);
}
System.out.println(arr[index]);
//向右递归遍历
if(2*index+2<arr.length) {
infixOrder(2*index+2);
}
}
//后序遍历
public void postOrder(int index) {
//如果数组为空
if(arr.length==0) {
System.out.println("数组为空");
}
//向左递归遍历
if(2*index+1<arr.length)
{
postOrder(2*index+1);
}
//向右递归遍历
if(2*index+2<arr.length) {
postOrder(2*index+2);
}
System.out.println(arr[index]);
}
}
public class LNR_Search {
public static void main(String[] args) {
int[] arr= {5,3,7,2,4,6,8};
ArrBinaryTree aB_T=new ArrBinaryTree(arr);
//前序遍历
//aB_T.preOrder();
//中序遍历
aB_T.infixOrder();
//后序遍历
//aB_T.postOrder();
}
}
运行结果:
三组数据分别对应前中后序遍历结果,可以看到中序输出是一个递增序列
方法一:递归实现
package JZ62_search_tree;
import java.util.*;
/*
* 递归,采用递归的方式,新开一个数组记录下中序遍历结果,然后返回数组第k-1的节点
*/
class TreeNode{
private int value;
private TreeNode cur_Node;
private TreeNode left_node;
private TreeNode right_node;
//初始化构造函数
public TreeNode(int data) {
this.value=data;
this.left_node=null;
this.right_node=null;
}
//
public TreeNode(TreeNode node) {
this.cur_Node=node;
}
//
public TreeNode getNode(TreeNode node){
return this.cur_Node;
}
public int getValue() {
return this.value;
}
public void setValue(int data) {
this.value=data;
}
public void setLeft(TreeNode left) {
this.left_node=left;
}
public TreeNode getLeft() {
return this.left_node;
}
public void setRight(TreeNode node) {
this.right_node=node;
}
public TreeNode getRight() {
return this.right_node;
}
//自定义输出
//public String toString() {
//return this.value+" ";
//}
}
public class Recursion {
public void createTree(int[] data,List<TreeNode> list) {
for(int i=0;i<data.length;i++) {
TreeNode node=new TreeNode(data[i]);
list.add(node);
}
//将节点加入二叉树中,顺序存储二叉树中 左右节点的下标与父节点的关系已知
for(int index=0;index<list.size()/2-1;index++) {
//左节点
list.get(index).setLeft(list.get(index*2+1));
//右节点
list.get(index).setRight(list.get(index*2+2));
}
//单独处理最后的一个父节点,因为当数组只有偶数个元素时,最后父节点可能只有一个子节点,
//index<=list.size()/2-1,index*2+2越界
int index=list.size()/2-1;
list.get(index).setLeft(list.get(index*2+1));
//当搜索二叉树有奇数个节点时,则最后一个父节点有右子节点
if(list.size()%2==1) {
list.get(index).setRight(list.get(index*2+2));
}
}
public void infix(TreeNode cur,List<TreeNode> mark,int k){
if(cur==null||k<=0) {
return ;
}
if(cur.getLeft()!=null) {
infix(cur.getLeft(), mark,k);
}
mark.add(cur);
if(cur.getRight()!=null) {
infix(cur.getRight(),mark,k);
}
}
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int[] data={5,3,7,2,4,6,8};
int k=3;
//int k=sc.nextInt();
//for(int i=0;i<size;i++) {
//data[i]=next.int();
//}
List<TreeNode> list=new ArrayList<TreeNode>();
List<TreeNode> mark=new ArrayList<TreeNode>();
Recursion r=new Recursion();
r.createTree(data,list);
TreeNode Root=list.get(0);
r.infix(Root,mark,k);
for(int i=0;i<mark.size();i++) {
System.out.println(mark.get(i).getValue());
}
System.out.print(mark.get(k-1).getValue());
}
}
方法二:非递归实现
递归的过程其实就是函数不断的调入,在计算机中每一个函数都是一个栈帧,函数的调入与完成对应入栈与出栈。
非递归版中序遍历,可以利用栈来模拟递归遍历,首先根入栈,然后令根节点的左孩子不断入栈直到为空,弹出栈顶,令其右孩子入栈,重复以上操作,直到遍历结束或者访问第k个节点为止。
package JZ62_search_tree;
import java.util.*;
/*非递归 死扣递归很多时候还是有必要的,它不仅是一种优美的思路,简洁的代码,
* 更体现的是对函数不断调入与回溯这一过程的整体把握,基于整个递归程序流程的理解再去写非递归会更简单。
*/
//java中TreeNode有些功能没有,还是自己实现比较好
class TreeNode{
private int value;
private TreeNode cur_Node;
private TreeNode left_node;
private TreeNode right_node;
//初始化构造函数
public TreeNode(int data) {
this.value=data;
this.left_node=null;
this.right_node=null;
}
//
public TreeNode(TreeNode node) {
this.cur_Node=node;
}
//
public TreeNode getNode(TreeNode node){
return this.cur_Node;
}
public int getValue() {
return this.value;
}
public void setValue(int data) {
this.value=data;
}
public void setLeft(TreeNode left) {
this.left_node=left;
}
public TreeNode getLeft() {
return this.left_node;
}
public void setRight(TreeNode node) {
this.right_node=node;
}
public TreeNode getRight() {
return this.right_node;
}
//自定义输出
//public String toString() {
//return this.value+" ";
//}
}
public class Not_Recursion {
public void createTree(int[] data,List<TreeNode> list) {
for(int i=0;i<data.length;i++) {
TreeNode node=new TreeNode(data[i]);
list.add(node);
}
//将节点加入二叉树中,顺序存储二叉树中 左右节点的下标与父节点的关系已知
for(int index=0;index<list.size()/2-1;index++) {
//左节点
list.get(index).setLeft(list.get(index*2+1));
//右节点
list.get(index).setRight(list.get(index*2+2));
}
//单独处理最后的一个父节点,因为当数组只有偶数个元素时,最后父节点可能只有一个子节点,
//index<=list.size()/2-1,index*2+2越界
int index=list.size()/2-1;
list.get(index).setLeft(list.get(index*2+1));
//当搜索二叉树有奇数个节点时,则最后一个父节点有右子节点
if(list.size()%2==1) {
list.get(index).setRight(list.get(index*2+2));
}
}
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int[] data={5,3,7,2,4,6,8};
int k=3;
//int k=sc.nextInt();
//for(int i=0;i<size;i++) {
//data[i]=next.int();
//}
List<TreeNode> list=new ArrayList<TreeNode>();
Not_Recursion n_R=new Not_Recursion();
n_R.createTree(data, list);
Stack<TreeNode> stack=new Stack<>();
TreeNode cur=list.get(0);
while(!stack.isEmpty()||cur!=null) {
//模拟中序遍历递归,首先根节点入栈,再将左子节点不断入栈,直到为空
if(cur!=null) {
stack.push(cur);
System.out.println("入栈: "+cur.getValue());
cur=cur.getLeft();//更新左子节点
}else {
cur=stack.pop();//弹出栈顶
System.out.println("出栈: "+cur.getValue());
if(--k==0) {
System.out.println("output: "+cur.getValue());
//去掉break,可以观察输出顺序
//break;
}
cur=cur.getRight();
}
}
}
}
运行结果
栈的一个特性是先入后出,可以看到根节点5先入栈,再是左子节点3和2,按照搜索二叉树性质,左叶子节点肯定是最小的,直到它为空开始出栈,可以观察到入栈的顺序是先序遍历的顺序,出栈的顺序就是我们中序遍历的顺序,就是一个递增序列的结果,自然第k小的结果很容易找出.
那么怎么决定我入栈和出栈的顺序呢?其实可以观察我们入栈操作stack.push是在更新左子节点和右子节点之前,这个和先序遍历是一致的,同样出栈操作是在更新左子节点之后,更新右子节点之前,他也是和中序遍历顺序一致.
参考链接
https://blog.nowcoder.net/n/8b7ca187606940f6856733ebd127efce?f=comment
https://blog.nowcoder.net/n/fa683a00f1a8445cad7c09f91b538265?f=comment
https://www.bilibili.com/video/BV1E4411H73v?from=search&seid=12494984529953555659