二叉树的概念:

一颗二叉树是节点的有限集合。二叉树由左子树和右子树组成,每一个非空的子树都可以称作一个独立的二叉树。

二叉树的特点:

 

  • 每个节点最多有两颗子树,即树的度最大为2;
  • 子树右左右之分,次序不能颠倒。

二叉树的分类:

满二叉树:不存在度为1的节点。只可能度为0和2 (节点个数N=(2^k) -1, k为深度,如下图满二叉树的节点个数为7 = (2 * 2 * 2) - 1)

完全二叉树:具有N个节点的结构与满二叉树的前n个节点的结构相同。称为完全二叉树(永远都有左子树,可能没有右子树)。

java 排序二叉树 java二叉树数据结构_创建二叉树

二叉树的性质:

(1)若规定根节点的层数为1,则一颗非空二叉树的第i层上最多有2^(i - 1)个节点。

(2)若规定只有根节点的二叉树深度为1,则深度为K的二叉树的最大节点数是2^K - 1。

(3)对任何一个二叉树,如果其叶节点个数为n0,度为2的非叶节点个数为n2,则有n0 = n2 + 1,n总 = n0 + n1 + n2;

(4)具有n个节点的完全二叉树的深度k为log2(n + 1)上取整。

(5)对于具有n个节点的完全二叉树,入股按照从上到下,从左到右的顺序对所有节点从0开始编号,则对于序号为i的节点有,

  • 若i > 0,双亲序号:(i - 1)/ 2 ;i = 0, 为根节点,无双亲节点
  • 若2i + 1 < n,左孩子编号为2i + 1,否则无左孩子。
  • 若2i + 1 < n,右孩子编号为2i + 2,否则无右孩子。

二叉树的存储结构:


顺序存储:


缺点:遇到单支树和一般二叉树会浪费大量存储空间。


java 排序二叉树 java二叉树数据结构_二叉树_02

链式存储:

优点:能很好的利用存储空间。适用于任何二叉树的存储

java 排序二叉树 java二叉树数据结构_二叉树_03

常见二叉树节点的定义:通常使用左右孩子表示法来描述二叉树

@Data
    @NoArgsConstructor
    static class Node<T> {

        /**
         * 左孩子
         */
        private Node<T> leftChild;

        /**
         * 右孩子
         */
        private Node<T> rightChild;

        /**
         * 数据
         */
        private T data;

        public Node(T data) {
            this.data = data;
        }
    }

 

二叉树的基本操作:

(1)创建二叉树

(2)先序遍历二叉树

(3)中序遍历二叉树

(4)后序遍历二叉树

(5)拷贝一颗二叉树

(6)层序遍历

(7)二叉树的销毁

(8)二叉树的镜像非递归

(9)求二叉树中节点的个数

(10)求二叉树中叶子节点的个数

(11)求二叉树中第K层节点的个数

(12)求二叉树的高度

(13)检测一颗二叉树是否为完全二叉树

(14)根据数据获取二叉树中的节点

(15)检测一个节点是否在二叉树中

(16)前序遍历的非递归实现

(17)中序遍历的非递归实现

(18)后续遍历的非递归实现

(19)二叉树的镜像递归实现

具体实现:

(1)创建二叉树(必须遵循先序遍历规则)

思路:

  • 先将数据按先序遍历的顺序存储在一个数组中。
  • 创建根节点,将数据赋值给根节点。
  • 创建左子树
  • 创建右子树
  • 遇到无用的操作符,该节点不被创建,并且函数返回。

代码如下:

private static int index = 0;

    public <T> Node<T> create(T[] data, Node<T> node, int i) {
        if (i >= data.length) {
            return null;
        }
        if (data[i] == null) {
            return null;
        }
        node.setData(data[index]);
        node.setChildLeft(create(data, new Node<>(), ++index));
        node.setChildRight(create(data, new Node<>(), ++index));
        return node;
    }

 

(2)先序遍历二叉树

思路:先遍历根节点,再遍历左子树,再遍历右子树

public <T> void preIterator(Node<T> node) {
        if (node != null && node.getData() != null) {
            System.out.print(node.getData() + ",");
            preIterator(node.getChildLeft());
            preIterator(node.getChildRight());
        }
    }

 

(3)中序遍历二叉树

/**
     * 中序遍历
     *
     * @param node
     * @param <T>
     */
    public <T> void inIterator(Node<T> node) {
        if (node != null && node.getData() != null) {
            inIterator(node.getChildLeft());
            System.out.print(node.getData() + ",");
            inIterator(node.getChildRight());
        }
    }

 

(4)后序遍历二叉树

/**
     * 后序遍历
     *
     * @param node
     * @param <T>
     */
    public <T> void postIterator(Node<T> node) {
        if (node != null && node.getData() != null) {
            postIterator(node.getChildLeft());
            postIterator(node.getChildRight());
            System.out.print(node.getData() + ",");
        }
    }

 

(5)拷贝一颗二叉树(先序遍历规则)

思路:先拷贝根节点,再拷贝左子树,再拷贝右子树。

/**
     * 同创建二叉树
     * @param src
     * @param <T>
     * @return
     */
    public <T> Node<T> copy(Node<T> src) {
        if (src == null || src.getData() == null) {
            return null;
        }
        Node<T> desc = new Node<>();
        desc.setData(src.getData());
        desc.setChildLeft(copy(src.getChildLeft()));
        desc.setChildRight(copy(src.getChildRight()));
        return desc;
    }

 

(6)层序遍历

思路:先遍历根节点,每遇到一个节点都先依次判断他的左右孩子是否为空,然后将其左孩子和右孩子加入到队列中。然后将队头节点出队列

/**
     * 层序遍历二叉树
     *
     * @param head
     * @param <T>
     */
    public <T> void levelIterator(Node<T> head) {
        if (head == null) {
            return;
        }
        Queue<Node<T>> queue = new LinkedBlockingQueue<>();
        queue.add(head);

        while (!queue.isEmpty()) {
            Node<T> node = queue.poll();
            System.out.print(node.getData());
            if (node.getLeftChild() != null) {
                queue.add(node.getLeftChild());
            }
            if (node.getRightChild() != null) {
                queue.add(node.getRightChild());
            }
        }
    }

 

(7)二叉树的销毁(必须遵循后续遍历规则,否则会出现空指针异常)

思路:要销毁一颗二叉树,应该先删除他的左右子树,再删除根节点。

/**
     * 销毁一颗二叉树
     *
     * @param head
     * @param <T>
     */
    public <T> void destroy(Node<T> head) {
        if (head == null) {
            return;
        }
        destroy(head.leftChild);
        destroy(head.rightChild);
        head.setData(null);
        head.leftChild = null;
        head.rightChild = null;

        // java中这一步没啥用。。函数内部改变引用指向不会影响外部。
        head = null;
    }

 

(8)二叉树的镜像非递归

思路:和层序遍历一样,当前节点入队列,左孩子入队列,右孩子入队列,交换队头节点的左右孩子

// 二叉树的镜像---非递归 
void MirrorBinTreeNor(PBTNode pRoot) {
	Queue* q = (Queue*)malloc(sizeof(Queue));
	QueueInit(q);
	QueuePush(q, pRoot);
	PBTNode tmp = NULL;
	while (!QueueEmpty(q)) {
		PBTNode cur = QueueFront(q);

		if (cur->LChild != NULL)
			QueuePush(q, cur->LChild);
		if (cur->RChild != NULL)
			QueuePush(q, cur->RChild);
		swapBTNodeNode(&(cur->LChild), &(cur->RChild));
		QueuePop(q, &tmp);
	}
}

 

(9)求二叉树中节点的个数

思路:总节点个数等于 ==  左孩子个数 + 右孩子个数 + 根节点个数(1),递归结束条件,当前节点为空,返回0(优化:当左右孩子为空时,直接返回,可以大幅减少递归深度);

/**
     * 二叉树节点个数
     *
     * @param head
     * @param <T>
     */
    public <T> int nodeCount(Node<T> head) {
        if (head == null) {
            return 0;
        }
        if (head.leftChild == null && head.rightChild == null) {
            return 1;
        }
        return nodeCount(head.leftChild) + nodeCount(head.rightChild) + 1;
    }

(10)求二叉树中叶子节点的个数

思路:叶子节点个数 == 叶子节点中左孩子的个数 + 叶子节点中右孩子的个数

/**
     * 二叉树叶子节点个数
     *
     * @param head
     * @param <T>
     */
    public <T> int leafNodeCount(Node<T> head) {
        if (head == null) {
            return 0;
        }
        if (head.leftChild == null && head.rightChild == null) {
            return 1;
        }
        return leafNodeCount(head.leftChild) + leafNodeCount(head.rightChild);
    }

 

(11)求二叉树中第K层节点的个数

思路:和求叶子节点个数类似,第K层中左孩子个数 + 第K层中右孩子个数;只是将结束条件变成了K == 1;(K最小为1时,节点为根节点,个数为1)

/**
     * 二叉树第k层节点个数
     * 第k层节点个数等于第k-1层左孩子个数+第k-1层右孩子个数
     *
     * @param head
     * @param k
     * @param <T>
     * @return
     */
    public <T> int levelKNodeCount(Node<T> head, int k) {
        if (head == null) {
            return 0;
        }
        if (k <= 0) {
            return 0;
        }
        if (k == 1) {
            return 1;
        }
        return levelKNodeCount(head.leftChild, k - 1) + levelKNodeCount(head.rightChild, k - 1);
    }

 

(12)求二叉树的高度

思路:判断当前节点是否为空,为空返回0,不为空返回1,将返回值赋值给high,比较左右子树的较大值,取大的。

/**
     * 二叉树的高度
     *
     * @param head
     * @param <T>
     * @return
     */
    public <T> int bTreeHigh(Node<T> head) {
        if (head == null) {
            return 0;
        }
        if (head.leftChild == null && head.rightChild == null) {
            return 1;
        }
        int leftHigh = bTreeHigh(head.leftChild);
        int rightHigh = bTreeHigh(head.rightChild);
        return Math.max(leftHigh, rightHigh) + 1;
    }

 

(13)检测一颗二叉树是否为完全二叉树

思路:完全二叉树的某个节点之前一定是一颗满二叉树。所有的节点度都为2。

找到这个节点。

这个节点的可能情况为:只有左孩子,没有左孩子也没有右孩子。

利用层序遍历,从上到下从左到右,就可以找到这个点。如果在这个点之后出现了任何一个节点有孩子,那这个二叉树就不是完全二叉树。

// 检测一棵树是否为完全二叉树 
int IsCompleteBinTree(PBTNode pRoot) {
	if (pRoot == NULL)
		return 0;
	// 标识是否找到了这个特殊的节点。
	int flag = 0;
	PBTNode cur;
	Queue q;
	QueueInit(&q);
	QueuePush(&q, pRoot);
	while (!QueueEmpty(&q)) {
		QueuePop(&q, &cur);
		if (flag == 1 && (cur->LChild != NULL || cur->RChild != NULL))
			return 0;
		if (cur->LChild != NULL && cur->RChild != NULL && flag == 0) {
			QueuePush(&q, cur->LChild);
			QueuePush(&q, cur->RChild);
		}
		if (cur->LChild == NULL && cur->RChild != NULL)
			return 0;
		if ((cur->RChild == NULL && cur->LChild == NULL) ||
			(cur->RChild == NULL && cur->LChild != NULL))
		{
			// 找到分界点。
			flag = 1;
		}	
	}
	return 1;
}

 

(14)根据数据获取二叉树中的节点

思路:先找左孩子,没找到(node == NULL)才找右孩子。

/**
     * 根据数据获取节点,找到则返回
     *
     * @param head
     * @param <T>
     * @return
     */
    public <T> Node<T> getNodeByData(Node<T> head, T data) {
        if (head == null) {
            return null;
        }
        if (head.data.equals(data)) {
            return head;
        }
        if (head.leftChild == null && head.rightChild == null) {
            return null;
        }
        Node<T> node = getNodeByData(head.leftChild, data);
        if (node == null) {
            node = getNodeByData(head.rightChild, data);
        }
        return node;
    }

 

(15)检测一个节点是否在二叉树中

思路:和获取二叉树中的节点相同,但是要判断pNode是否为空。

/**
     * 判断节点是否在二叉树中
     *
     * @param head
     * @param <T>
     * @return
     */
    public <T> boolean hasNode(Node<T> head, Node<T> node) {
        if (node == null || head == null) {
            return false;
        }
        if (head == node) {
            return true;
        }
        if (head.leftChild == null && head.rightChild == null) {
            return false;
        }
        return hasNode(head.leftChild, node) || hasNode(head.rightChild, node);
    }

 

(16)前序遍历的非递归实现

思路:先将当前节点的右孩子入栈,然后一直往左遍历(左孩子不入栈),如果遇到的节点为空,就出栈。并将当前节点设置为栈中的节点。

// 循环先序遍历二叉树
void PreOrderNor(PBTNode pRoot) {
	if (pRoot == NULL)
		return;
	PBTNode cur = pRoot;
	Stack s;
	StackInit(&s);
	while (1) {
		printf("%c ", cur->data);
		if (cur->RChild != NULL)
			StackPush(&s, cur->RChild);
		cur = cur->LChild;
		if (cur == NULL) {
			int ret = StackPop(&s, &cur);
			if (ret == 0)
				return;
		}
	}
}

 

(17)中序遍历的非递归实现

思路:

(1)一直找到当前树的最左节点,并将遇到的节点入栈。

(2)出栈打印左孩子,判断当前节点有无右孩子,无继续出栈,若有,则继续第一步

//循环中序遍历二叉树
void InOrderNor(PBTNode pRoot) {
	if (pRoot == NULL)
		return;
	PBTNode cur = pRoot;
	PBTNode tmp = NULL;
	Stack s;
	StackInit(&s);
	while (1) {

		while (cur != NULL) {
			StackPush(&s, cur);
			cur = cur->LChild;
		}

		// ret表示pop的状态,0表示空栈
		int ret = StackPop(&s, &tmp);
		if (!ret)
			return;

		printf("%c ", tmp->data);
		if (tmp->RChild)
			cur = tmp->RChild;

	}
	
}

 

(18)后续遍历的非递归实现

思路:

(1)将当前节点入栈,将当前节点的右孩子入栈,直到找到当前树的最左节点。

(2)看最左节点有无右孩子,有跳到(1),无则打印这个最左节点。

(3)出栈,将出栈的这个节点赋值给当前节点,判断有无左右孩子,有跳到(1)

//循环后续遍历二叉树
void PostOrderNor(PBTNode pRoot) {
	if (pRoot == NULL)
		return;
	Stack s;
	StackInit(&s);

	PBTNode cur = pRoot;

	// 当前节点
	PBTNode tmp = NULL;

	// 用来记录上一次出栈的节点,以便判断这个节点是否应该继续向下遍历
	PBTNode preTmp = NULL;

	//放入根节点
	StackPush(&s, cur);

	while (1) {

		// 一直向左,入栈顺序为,cur cur->RChild  cur->LChild
		while (cur != NULL)
		{
			if (cur->RChild)
				StackPush(&s, cur->RChild);
			if (cur->LChild)
				StackPush(&s, cur->LChild);
			cur = cur->LChild;
		}
		if (cur == NULL) {

			tmp = StackTop(&s);

			//栈为空退出
			if (tmp == NULL)
				return;

			//判断当前点的子节点是否是上一次遍历的。如果是则代表这颗树的子节点已经遍历完了,就直接输出当前节点
			if (tmp->LChild == preTmp || tmp->RChild == preTmp) {
				printf("%c ", tmp->data);
				StackPop(&s, &tmp);
				preTmp = tmp;
				continue;
			}

			// 不直接遍历则看他的左右孩子是否为空,不为空就入栈
			if (tmp->LChild || tmp->RChild) {
				cur = tmp;
				preTmp = tmp;
				continue;
			}

			//叶子节点,直接打印。
			StackPop(&s, &tmp);
			preTmp = tmp;
			printf("%c ", tmp->data);

		}		
	}
}

 

(19)二叉树的镜像递归实现

思路:遇到一个非空的节点就交换他们的左右孩子。

// 二叉树的镜像---递归 
void MirrorBinTree(PBTNode pRoot) {
	if (pRoot) {
		swapBTNodeNode(&(pRoot->LChild), &(pRoot->RChild));
		MirrorBinTree(pRoot->LChild);
		MirrorBinTree(pRoot->RChild);
	}
}

以上程序的运行结果:

java 排序二叉树 java二叉树数据结构_二叉树面试题_04