leetcode算法练习
二叉树理论基础
二叉树的种类:
- 满二叉树——深度从1开始 总结点数为(2^k)-1
- 完全二叉树:除了底层 其他层都是满的(从左到右 从上到下编号连续才可以)
- 二叉搜索树:结点便于搜索 一定有顺序(左孩子小于父结点 右孩子大于父结点) 搜索一个结点的时间复杂度是logn级别的
- 平衡二叉搜索树(AVL树):符合二叉平衡树的要求且左子树和右子树高度的绝对值不能超过1
二叉树的存储方式:
链式存储:利用左右指针
线性存储:利用数组
找左孩子:2 * i + 1
找右孩子:2 * i + 2
在算法中如果要求传入一个二叉树 那么一般都采用链式存储 构造一个结点 左右指针分别指向构造的下一个结点 把这个头结点传入功能函数中即可
二叉树的遍历:
与图论中的遍历是一样的:深度优先遍历 广度优先遍历
深度优先搜索:一般使用递归——前序/中序/后序都是 底层利用栈 一般都是递归来实现/迭代法实现
广度优先搜索:一层一层遍历或一圈一圈遍历——层序遍历就是 利用队列 迭代法实现
二叉树的定义:
public class TreeNode {
int val;
// 指针 指向左孩子
TreeNode left;
// 指针 指向右孩子
TreeNode right;
// 无参构造
TreeNode() {}
// 有参构造
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
144.145.94.递归遍历
递归三部曲:
- 确定递归函数的参数和返回值(不用一次性确定 可以边写边加 一般是一个根结点 一个数组/返回值一般是 void)
- 确定终止条件
- 确定单层递归的逻辑
// 前序遍历·递归·LC144_二叉树的前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<Integer>();
preorder(root, result);
return result;
}
// 参数传入根结点和数组 数组里存放前序遍历结果
public void preorder(TreeNode root, List<Integer> result) {
// 遇到空结点 就返回
if (root == null) {
return;
}
// 数组放遍历的元素 就是根
result.add(root.val);
// 左
preorder(root.left, result);
// 右
preorder(root.right, result);
}
}
// 中序遍历·递归·LC94_二叉树的中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
inorder(root, res);
return res;
}
void inorder(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
inorder(root.left, list);
list.add(root.val); // 注意这一句
inorder(root.right, list);
}
}
// 后序遍历·递归·LC145_二叉树的后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
postorder(root, res);
return res;
}
void postorder(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
postorder(root.left, list);
postorder(root.right, list);
list.add(root.val); // 注意这一句
}
}
迭代遍历
也是使用栈这种数据结构来实现
// 前序遍历顺序:根-左-右,入栈顺序:根-右-左
class Solution {
// 传入根结点
public List<Integer> preorderTraversal(TreeNode root) {
// 存放遍历结果
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
// 定义一个栈 存放的元素是TreeNode 一般是个结构体 包含value 左右指针
Stack<TreeNode> stack = new Stack<>();
// 根结点入栈
stack.push(root);
// 循环处理栈 栈不为空就一直执行
while (!stack.isEmpty()){
// 先把放入的根结点
TreeNode node = stack.pop();
// 弹出存进数组
result.add(node.val);
// 先入右孩子 再入左孩子
if (node.right != null){
stack.push(node.right);
}
if (node.left != null){
stack.push(node.left);
}
}
return result;
}
}
// 中序遍历看下一部分代码
// 可以将前序的代码 左右颠倒一下 则变为根右左 再将数组反转 变为 左右根
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()){
TreeNode node = stack.pop();
result.add(node.val);
if (node.left != null){
stack.push(node.left);
}
if (node.right != null){
stack.push(node.right);
}
}
// 反转数组
Collections.reverse(result);
return result;
}
}
前序遍历:遍历顺序和处理顺序(数字放入数组的顺序)是一致的:54126
中序遍历:访问肯定是从根结点5开始 但是遍历是从1开始的 ——故遍历顺序和处理顺序不一致:14256
如何处理中序?
- 利用一个指针 将对应的元素存入数组
- 在遍历的时候要用栈来记录我们遍历过的顺序 因为处理元素的时候 其实是按照遍历的顺序逆向输出的
- 先访问5 存入栈——栈内5
- 存入4——栈内5 4
- 存入1——栈内5 4 1
- 一路向左访问 弹出1 加入数组——栈内5 4 数组内1
- 这时候1的右 也是空 所以访问栈内的元素 弹出4——栈内5 数组内1 4
- 4的右孩子是2 2入栈——栈内5 2 2的左 2弹出——栈内5 数组内1 4 2
- 2的右也为空 5弹出——栈内为空 数组内1 4 2 5
- 5的右孩子不为空 3加入栈——栈内6
- 6的左孩子为空 弹出——数组内1 4 2 5 6
10.6的右孩子为空 栈内也为空 结束
总结:
先一路向左 到没有左孩子为止 在弹出栈内元素 看该元素的右孩子是不是也为空 没有就再接着弹出 有就入栈
// 中序遍历和处理顺序不一样 不能直接修改前序的代码
// 中序遍历顺序: 左-根-右 入栈顺序: 左-右
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
// 结果集
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
// 指针
TreeNode cur = root;
// 指针为空 栈也为空 就终止遍历
while (cur != null || !stack.isEmpty()){
// 指针不为空 就加入该元素 指针往左走 一路向左
if (cur != null){
stack.push(cur);
cur = cur.left;
}else{
// 如果指针为空 栈里弹出加入数组
cur = stack.pop();
result.add(cur.val);
// 指针右移
cur = cur.right;
}
}
return result;
}
}