树—最“有套路”的数据结构

前言

标题用“有套路”来形容一种数据结构,似乎有点不尊重的意思。不过,我倒是觉得,一种实用的学科,就是应该产生一点套路,这才能发挥体系化研究的优势,套路就是一种保证:在不投入更多创造性与努力的情况下,依旧能获得比起随意进行相关操作更好的结果。一门成熟的学科都应如是,如果研究许久,在学科所研究的许多问题的实践上还不如一些“天赋”“灵感”,那就不得不说这门学科的“伪科学”或者“水分”还是蛮大的了。

言归正传,这篇文章将会是一系列寻找算法与数据结构的文章的开篇,树由于其特性,是递归、分治等等重要算法思想的典型载体,同时套路性较强又具有一定规律和难度,上手后,也可以获得总结其他算法“套路”的必要经验。作为一个训练的开头,还是很合适了。

树的定义与理解

先简要谈谈树的抽象定义:树本质上是一种无向图(图:由顶点与路径构成的数据结构),其中,任意两个顶点之间有且只有一条路径。简而言之,树是一种具有特殊性质的图。

树的结构非常直观,而且树的大多数结构具有一个重要性质:递归。主要来说,就是树具有某一性质时,往往其子树也具有同样的性质。比如说,一个树如果是二叉搜索树,其子树也必须是二叉搜索树。

根据这样的性质,遇到树的问题,很自然会考虑如何合理使用递归算法,其实质就是:分解为子问题,最后解决基本情况,把复杂的递归过程交给计算机来处理。所以,树类型代码的特点就是简洁(不过换句话说,由于太简洁,很多思维过程又交给了计算机,有时候写对了没有都不知道:) )

所以,面试官往往是在用树来考察你对递归算法的理解。

一般来说,树具有以下可能的形状: 普通二叉树、平衡二叉树(即叶子节点的深度相差不超过1)、完全二叉树(除了最后一层外,每层节点全部填满,最后一层若不满,必须是右边的节点缺少;每层都填满的,被称为完美二叉树)、四叉树(quadtree,即每个节点最多有四个孩子)、N叉树(每个节点最多有N个孩子)。

树的遍历

先谈谈为什么要遍历。其实对于一个数据结构的基础算法(不讨论那些奇技淫巧),重要的不过增查改删(CRUD)。对于树这样一个结构,在没有先验知识的情况下,为了使得代码量可控,考虑到树的结构是固定的(或者说每个节点/子树都具有一样的结构),我们就得依照其连接关系借助递归方法,以一种特定的顺序进行访问。当全部的节点都被访问时,这样的操作就是一个遍历。

前序遍历(pre-order traversal):

访问顺序:根-左子树-右子树

简单实现代码:

1public void preorderTraverse(TreeNode root){2  visit(root); // 此步执行遍历的功能,比如说打印、比较、存储等等3  preorderTraverse(root.left);4  preorderTraverse(root.right);5}

应用场景:在树中进行搜索、创建一棵新的树(没有根,左右子树创建后不便于连接)。

中序遍历(inorder Traverse)与后序遍历(postorder Traverse):

基本同前序遍历,只是顺序上,中序是左-根-右,后序是左-右-根。

中序遍历可写作这样的递归形式:

1public void inorderTraverse(TreeNode root){2  inorderTraverse(root.left);3  visit(root); // 此步执行遍历的功能,比如说打印、比较、存储等等4  inorderTraverse(root.right);5}

可以看到,此时也并不复杂,只是单纯地换序而已,将在根处的操作移动到了中间,规律明显,代码也易于阅读。

对于中序遍历,其应用场景最常见的是二叉搜索树,按这样的顺序遍历出来,输出的结果是按大小顺序的(比如说递增)。

关于后序遍历,如果在对某个节点进行分析时,需要其左右子树的信息(大小、存在与否等等),那么可以考虑后序遍历,仿佛就是在修剪一棵树的叶子,可以从外向内不断深入修剪。

由于三种遍历说的前中后都是针对根节点,国内也有教材把上述遍历方式称为,先根遍历中根遍历后根遍历,前中后决定的只是根的位置,左子树都在右子树之前,这样记忆可能简单些(虽然最好的记忆方法永远是实现几个代码)。

不过,递归写法虽然容易,非递归写法也需要掌握。

这里提供一种前序遍历的非递归写法的分析思路:

主要考虑用栈来模拟上面的递归(递归本质上也是操作系统/OS在帮你压栈,所以递归深度过深时报错也是所谓“stack overflow”,即栈溢出):

以下出自leetcode144题,即二叉树前序遍历:

 1public List<Integer> preorderTraversal(TreeNode root){ 2  List<Integer> res = new ArrayList<>(); // 用于保存遍历结果 3  Stack<TreeNode> stack = new Stack<>(); 4  TreeNode curr = root; // 用于指示当前节点 5  while(curr != null || !stack.isEmpty()){ 6        if(curr != null){ 7      res.add(curr.val); 8        stack.push(curr); 9        curr = curr.left; // 考虑左子树10    } else{11      // 节点为空,弹栈,回溯到上一层12      curr = stack.pop();13      // 此时考虑右子树14      curr = curr.right;15    }16  }17  return res;18}

类似地,中序遍历可以写作以下非递归形式,除了稍微改变顺序以外,几乎跟前序遍历一样:

 1public List<Integer> inorderTraversal(TreeNode root) { 2        List<Integer> res = new ArrayList(); 3        Stack<TreeNode> stack = new Stack(); 4        TreeNode curr = root; // 指示当前位置 5 6        while(curr != null || !stack.isEmpty()){ 7            if(curr != null){ 8                stack.push(curr);  9                curr = curr.left;  //先入栈,并指向左节点10            } else{11                //当前节点为空时,出栈,并进行操作,随后指向右节点12                curr = stack.pop();13                res.add(curr.val);14                curr = curr.right;15            }16        }1718        return res;19    }

不同于上面两个,后序遍历的非递归略微难写一点,主要是因为需要考虑根节点的问题,在整个遍历过程中,根节点会被访问两次,需要判断是否右子树已经被访问过了,才能确认根节点是否需要被保存。最简单粗暴的想法就是用hashset保存已经访问过的节点:

 1class Solution { 2    public List<Integer> postorderTraversal(TreeNode root) { 3        List<Integer> res = new ArrayList(); 4        Stack<TreeNode> stack = new Stack(); 5        Set<TreeNode> set = new HashSet<>(); 6        TreeNode curr = root; 7 8        while(curr != null || !stack.isEmpty()){ 9              // 首先不断向左子树运动10            while(curr != null && !set.contains(curr)){11                stack.push(curr);12                curr = curr.left;13            }14              // 此时不能直接保存根节点再去右子树,所以只能利用peek来寻找右子树15            curr = stack.peek();16              // 若右子树为空,或者已经访问过一次这个节点,可以弹出17            if(curr.right == null || set.contains(curr)){18                res.add(curr.val);19                set.add(curr);20                stack.pop();21                  // 若栈此时已经弹空,可以返回结果22                if(stack.isEmpty()){23                    return res;24                }25                curr = stack.peek();26                curr = curr.right;27            } else{28                  // 若不然,先向右移动,保存该点到hashset中确认已经访问过一次29                set.add(curr);30                curr = curr.right;31            }32        }33        return res;34    }35}

上述解法是利用hashset来保存根节点是否已经被越过一次,但是重要的其实只是确认根节点的右子树是否被访问过,所以只需要知道上一个节点是不是其右子树就行,因此保留一个上一个节点的变量即可:

 1class Solution { 2    public List<Integer> postorderTraversal(TreeNode root) { 3                 List<Integer> res = new ArrayList(); 4          Stack<TreeNode> stack = new Stack(); 5          TreeNode curr = root; 6          TreeNode pre = null; 7 8          while(curr != null || !stack.isEmpty()){ 9              if(curr != null){10                  stack.push(curr);11                  curr = curr.left;12            } else{13                  TreeNode temp = stack.peek();14                  // 考虑是否变为右子树15                  if(temp.right != null && temp.right != pre){16                      curr = temp.right;17                } else{18                      res.add(temp.val);19                      pre = temp;20                      stack.pop();21                }22            }23        }24          return res;25    }26}

官方题解的方法则更为巧妙一点,即考虑将相关的内容逆序保存,这样巧妙地规避了根节点不便处理的问题:实现一个右、左、中的保存顺序,实现时还是按中、右、左来进行,保存数字时逆序。

利用了LinkedList特有的addFirst方法,将新出现的数值插入到链表的开头,同事时,由于stack的顺序是先进后出,所以看似是中、左、右,出栈后又是中、右、左,由于保存的顺序是每次加入到链表开头,所以实际上进行了一个逆序,完成了一个左、右、中的过程:

 1class Solution { 2  public List<Integer> postorderTraversal(TreeNode root) { 3    LinkedList<TreeNode> stack = new LinkedList<>(); 4    LinkedList<Integer> output = new LinkedList<>(); 5    if (root == null) { 6      return output; 7    } 8 9    stack.add(root);10    while (!stack.isEmpty()) {11      TreeNode node = stack.pollLast();12      output.addFirst(node.val);13      if (node.left != null) {14        stack.add(node.left);15      }16      if (node.right != null) {17        stack.add(node.right);18      }19    }20    return output;21  }22}

除了使用那么复杂的办法,也有一种“投机取巧”的办法,就是先做中-右-左的遍历,然后把遍历结果逆序,也就得到了后序遍历结果了。修改我们上面出现过的前序遍历代码也不难得到类似结果:

 1public List<Integer> postorderTraversal(TreeNode root){ 2  List<Integer> res = new ArrayList<>(); // 用于保存遍历结果 3  Stack<TreeNode> stack = new Stack<>(); 4  TreeNode curr = root; // 用于指示当前节点 5  while(curr != null || !stack.isEmpty()){ 6        if(curr != null){ 7      res.add(curr.val); 8        stack.push(curr); 9        curr = curr.right; // 考虑右子树10    } else{11      // 节点为空,弹栈,回溯到上一层12      curr = stack.pop();13      // 此时考虑右子树14      curr = curr.left;15    }16  }17  Collections.reverse(res); // 直接使用内置的逆序,不必在插入时一一进行操作18  return res;19}

当然,如果要多做一点优化,可以模仿前一个结果,在添加到res时使用addFirst方法。对于java实现,这里有一个小细节,在声明时,必须使用LinkedList,因为List不提供addFirst方法接口。

经典例题回顾:

讲了基本的定义与三个遍历的递归与非递归写法,其实树的基础知识已经足够了,基本上遇到题目稍做变化就行了,不信?直接上一些看似复杂的题目看看吧。

leetcode250题(后序遍历):统计有多少个子树拥有相同的数字。

Given a binary tree, count the number of uni-value subtrees.
A Uni-value subtree means all nodes of the subtree have the same value.
给定一个二叉树,统计其uni-value子树的个数。
一个uni-value子树指的是这个子树的全部节点的值相同。

一段有问题的代码(对于测试样例[5,1,5,5,5,null,5],正确答案4,输出结果2):

 1class Solution { 2    int count = 0; 3    public int countUnivalSubtrees(TreeNode root) { 4        postorder(root); // 从根节点开始遍历 5        return count; 6    } 7 8    public boolean postorder(TreeNode root){ 9        if(root == null) return true;10        if(postorder(root.left) && postorder(root.right)){11            if(root.left != null && root.left.val != root.val) return false;12            if(root.right != null && root.right.val != root.val) return false;13            count++;14            return true;15        }16        return false;17    }18}

可通过的代码:

 1class Solution { 2    int count = 0; 3    public int countUnivalSubtrees(TreeNode root) { 4        postorder(root); 5        return count; 6    } 7 8    public boolean postorder(TreeNode root){ 9        if(root == null) return true;10        if(postorder(root.left) & postorder(root.right)){11            if(root.left != null && root.left.val != root.val) return false;12            if(root.right != null && root.right.val != root.val) return false;13            count++;14            return true;15        }16        return false;17    }18}

在完成这题的过程中,我犯了一些错误,主要在于没有深刻理解递归:

  1. 在使用判断条件postorder(root.left)以及postorder(root.right)时,实际上已经发生了递归;(所以假定这题是前序遍历或中序遍历会很难完成代码的构造)
  2. 在java中,使用&&会出现短路,即前半部分已经false了之后,会自动不执行后半部分,因此为了逻辑的正确,此处只能使用&。

leetcode230题:

求在二叉搜索树(BST)中第k小的值,本题主要考虑使用中序遍历,因为二叉搜索树中序遍历是一个递增的数列:

唯一需要注意的就是左子树遍历完成后,有可能还没有达到k,那么可以设定Integer.MAX_VALUE作为没有找到的标志,防止未找到。

这个算法遍历的时间空间复杂度是O(k),最坏的情况下有可能达到O(N)。(在leetcode平台上打败了100%的java程序)

 1class Solution { 2    int count = 0; 3    public int kthSmallest(TreeNode root, int k) { 4        if(root.left != null){ 5            int res = kthSmallest(root.left, k); 6            if(res != Integer.MAX_VALUE) return res; // 若未能寻找到,应继续向右搜索 7        }  8        count++; 9        if(count == k) return root.val;10        if(root.right != null) return kthSmallest(root.right, k); // 题目中保证了能搜索到,所以此处不必再加判断1112        return Integer.MAX_VALUE; // 若在左侧节点未寻找到,返回这个标志13    }14}

相比之下,官方题解则先将全部的节点遍历到一个数组中,然后再寻找第k个,问题就在于将时间复杂度因而扩大到了O(N)。(使用这个方法,时间上只能打败49.01%的程序)

 1class Solution { 2  public ArrayList<Integer> inorder(TreeNode root, ArrayList<Integer> arr) { 3    if (root == null) return arr; 4    inorder(root.left, arr); 5    arr.add(root.val); 6    inorder(root.right, arr); 7    return arr; 8  } 910  public int kthSmallest(TreeNode root, int k) {11    ArrayList<Integer> nums = inorder(root, new ArrayList<Integer>());12    return nums.get(k - 1);13  }14}

leetcode366题:

Given a binary tree, collect a tree‘s nodes as if you were doing this: Collect and remove all leaves, repeat until the tree is empty.

给定一个二叉树,返回一个树的节点的集合的集合,每次都收集并移除当前的所有的叶子节点,直到整个树为空。

这是一个很有趣的题目,因为如果从上向下遍历,是不难的,直接使用BFS(宽度优先搜索),这样的方式又叫“层次遍历”(level-order traversal);然而,从下往上则无这个顺序,左右子树的深度非常有可能不同,而叶子节点只需要左右子树不存在即可。

本题可采用了刚才所谓的“剥洋葱”的办法,因此可以先考虑后序遍历(本题算是一种变体):

 1class Solution { 2    public List<List<Integer>> findLeaves(TreeNode root) { 3        List<List<Integer>> res = new ArrayList(); 4          while(root != null){ 5              List<Integer> curr = new ArrayList(); 6              root = upward(root, curr); 7              res.add(curr); 8        } 9          return res;10    }1112      public TreeNode upward(TreeNode root, List<Integer> curr){13          // 函数的功能是将所有叶子节点保存到curr中,然后把叶子节点置为null14          if(root == null) return null;15          if(root.left == null & root.right == null){16              // 如果是叶子节点,加入curr,然后返回null17              curr.add(root.val);18              return null;19        }20          // 如果不是叶子节点,递归调用其左右子树,保存新的情况21          root.left = upward(root.left, curr);22          root.right = upward(root.right, curr);23          return root;24    }25}

这一题算是使用了一种变体方法,不过本质上还是一种后序遍历。

当然,这题也可以不修改树,直接考虑DFS(深度优先搜索)再回溯,但是就需要引入一个判断深度的指标来进行控制了:

 1class Solution { 2    public List<List<Integer>> findLeaves(TreeNode root) { 3        List<List<Integer>> res = new ArrayList(); 4          postorder(root,res); 5          return res; 6    } 7 8      public int postorder(TreeNode root, List<List<Integer>> res){ 9          if(root == null) return -1; // 空节点置为-1,因为叶子节点是010          // 递归调用获取当前深度(准确的说,其实是从叶子节点开始的高度)11          int depth = 1 + Math.max(postorder(root.left,res),postorder(root.right,res));12          // 根据java语法,如果还没有list对象,需要先创建13          if(depth >= res.size()) res.add(new ArrayList());14          // 将节点加入对应深度,由于递归必然从最深处开始,顺序不会有问题15          res.get(depth).add(root.val);16          return depth;17    }18}

leetcode285:

Given a binary search tree and a node in it, find the in-order successor of that node in the BST.

The successor of a node p is the node with the smallest key greater than p.val.

给定一个二叉搜索树,并给定一个节点,寻找这个节点的中序遍历的后续节点。

寻找二叉搜索树的中序遍历后继,可以从递归和迭代两种方法完成,由于已经暗示明白了,直接使用中序遍历即可解决这一问题:

递归法依旧有容易阅读的优点,不过构造时不必想太复杂,直接使用前序节点来保存前一个数据即可。(此时已经达到了不错的结果,时间达到了92%)

 1class Solution { 2    TreeNode pre = null; 3    TreeNode ans = null; 4    public TreeNode inorderSuccessor(TreeNode root, TreeNode p) { 5        if(root == null || p == null) return null; 6        inorderSuccessor(root.left, p); 7        if(pre == p){ // 条件也可改为 pre != null && pre.val == p.val 8              // 如果其前继节点已经满足条件,则保存结果 9            ans = root;10        }11        pre = root;12        inorderSuccessor(root.right,p);13        return ans;14    }15}

非递归方法:这一方法直接改自中序遍历,不过实际上没有充分利用BST暗含的规律,所以速度不是最快的,仅仅达到了28.88%。

 1class Solution { 2    public TreeNode inorderSuccessor(TreeNode root, TreeNode p) { 3        if(curr == null || p == null) return null; 4          Stack<TreeNode> stack = new Stack(); 5          TreeNode curr = root; 6          boolean foundP = false; 7          while(root != null || !stack.isEmpty()){ 8              if(curr != null){ 9                  stack.push(curr);10                  curr = curr.left;11            } else{12                  curr = stack.pop();13                  if(foundP == true){14                      return curr;15                }16                  if(curr.val == p.val){17                      foundP = true;18                }19                  curr = curr.right;20            }21        }22          return null;23    }24}

不得不说,使用遍历来解决树问题,实在是颇有“一招鲜吃遍天”的感觉,甚至略加改进就能解决hard级的leetcode99题。

leetcode99题:

Two elements of a binary search tree (BST) are swapped by mistake.

Recover the tree without changing its structure.

一个二叉搜索树的两个元素被交换了。

复原这个树,使得它符合BST结构,但不改变树的整体结构。

这题就是恢复二叉搜索树的问题。看到BST,几乎是一个中序遍历的问题。本题需要注意的地方就是,被交换的节点存在两种可能性,一种是相邻节点的顺序反了,那么,直接交换值即可,另外一种是相隔的节点顺序错了,因此还需要保存第二次发生前序节点大于当前节点的位置。(以下主要由中序遍历模板改出)

非递归:

 1class Solution { 2    public void recoverTree(TreeNode root) { 3        Stack<TreeNode> stack = new Stack(); 4        TreeNode prev = null, curr = root; 5        TreeNode first = null, second = null; // 用于保存改变的第一个和第二个位置 6 7        while(curr != null || !stack.isEmpty()){ 8            if(curr != null){ 9                stack.push(curr);10                curr = curr.left;11            } else{12                curr = stack.pop();13                  // 如果发生了前序节点大于当前节点,分两种情况14                if(prev != null && prev.val > curr.val){15                      // 如果是第一次遇到,保存first为前序,second为当前16                    if(first == null){17                        first = prev;18                        second = curr;19                    } else{20                          // 如果第二次遇到,只重新保存当前值到second中21                        second = curr;22                    }23                }24                prev = curr;25                curr = curr.right;26            }27        }28        int temp = first.val;29        first.val = second.val;30        second.val = temp;31    }32}

递归:

 1class Solution { 2    TreeNode first = null; 3    TreeNode second = null; 4    TreeNode prev = null; 5    public void recoverTree(TreeNode root) { 6        inorder(root); 7        int temp = first.val; 8        first.val = second.val; 9        second.val = temp;10    }1112    public void inorder(TreeNode root){13        if(root == null) return;14        inorder(root.left);15          // 对前序节点进行判断16        if(prev != null && prev.val > root.val){17              // 第一次遇到则全部保存18            if(first == null){19                first = prev;20                second = root;21            } else{22                  // 第二次遇到只保存second23                second = root;24            }25        }26          // 更新prev节点27        prev = root;28        inorder(root.right);29    }30}

简要总结

树的题目大多来自三种遍历的变体,一般而言,稍作一些修改就能得到一个能运行的结果(实在不行先全部遍历一遍保存到数组里来解决);不过,为了得到最优解,往往还需要根据题目已有的一些条件或者性质,对递归进行一些优化(如结束递归的条件,递归的方式等等);有时递归不便于进行一些细节操作时,可以考虑用非递归写法(当然,由于递归写法解题有时太轻松,出于面试难度问题,面试官也往往会要求非递归,也算是为学习非递归多一个理由吧!)。

原文地址:https://www.cnblogs.com/mingyu-li/p/12388360.html

时间: 2024-10-14 10:20:03

树—最“有套路”的数据结构的相关文章

[CDQ分治][Treap][树状数组]Theresa与数据结构

Description 这是个复杂的世界.人类社会,自然界,还有地球之外的银河--每一天日出日落,人来人往,步履匆匆.究竟是为什么呢?那支配着一切的至高无上的法则又是否存在呢?Theresa知道,这个问题并不是一朝一夕就可以解答的,只有在仔细.深入的观察和思考以后,才有可能将所有支离破碎的线索联系起来,从而隐约窥见真实的答案.于是,Theresa经常思考生活中遇到的大大小小的问题.为什么港台出版的书籍里印刷的汉字她一个也不认识呢?为什么隔夜的白开水中富含一氧化二氢呢?为什么每年都有一段时间Gma

线段树基础知识----(基础数据结构)--(一)

1.定义 引入:为什么要使用线段树而不用数组模拟呢? answer:因为有些题用数组来做就要超时,用线段树的O(log(n))的时间复杂度刚好可以求解 毫无疑问线段树是一种数据结构,但是它实际是一个类似树状的链表结构(个人认为) ///还是要正经一点(照搬教科书)----------- / ////////////////////////////////////////////////////////////////////// 线段树定义:线段树是一种二叉搜索树,与区间树相似,它将一个区间划分

CDOJ 838 母仪天下 树状数组 (2014数据结构专题

母仪天下 Time Limit: 1 Sec  Memory Limit: 162 MB 题目连接 http://acm.uestc.edu.cn/#/problem/show/838 Description 富庶的建业城中,有一条格格不入的长街,名曰跳蚤街,被战争所致的孤儿,聚集于此.全国的经济都在为战争服务之时,也无人顾得了这里了. 除了两位夫人. 大乔小乔每天都会带着一些食物来到跳蚤街,分给某一位孩子.为了避免分配不均,她们时常会询问一个区域内食物的总量,然后进行调整以保证每个孩子都有足够

数据结构学习笔记(树、二叉树)

树(一对多的数据结构) 树(Tree)是n(n>=0)个结点的有限集.n=0时称为空树.在任意一颗非空树种: (1)有且仅有一个特定的称为根(Root)的结点: (2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1.T2........Tn,其中每一个集合本身又是一棵树,并且称为根的子树. 对于树的定义还需要强调两点:1.n>0时根结点是唯一的,不可能存在多个根结点,数据结构中的树只能有一个根结点.2.m>0时,子树的个数没有限制,但它们一定是互不相交的. 结点

数据结构之2-3-4树

2-3-4树是一种阶为4的B树.它是一种自平衡的数据结构,可以在O(lgn)的时间内查找.插入和删除,这里的n是树中元素的数目.2-3-4树和红黑树是等价的,也就是每个红黑树都可以转化为一颗2-3-4树,每个选择操作也和2-3-4树中的分裂操作对应. 2-3-4树是这样一种数据结构,满足如下性质: 1) 每个节点每个节点有1.2或3个key,分别称为2-node,3-node,4-node. 2) 每个节点的keys把区间进行了划分,以4-nde为例,key1.key2.key3分别夹在subt

JavaScript数据结构-树

我觉得这社会上,也不差钱好多人,可能好多人也不差权力,但是我觉得能得到这种满足的也不多. –郭小平<临汾红丝带学校校长> ? 树是计算机科学中经常用到的一种数据结构.树是一种非线性的数据结构,以分层的方式存储数据.是被用来存储具有层级关系或有序的数据,比如文件系统中的文件. 二叉树 二叉树,每个节点最多有两个子树的树结构.二叉树是一种特殊的树,也是一个连通的无环图. 二叉查找树 ? 二叉查找树是一种特殊的二叉树,其相对较小的值保存在左节点中,较大的值保存在右节点中.这一特性使其查找效率很高.

数据结构与算法(4)树

树的基本概念: 1.结点的度 结点的度是子结点的个数.例如:结点1有三个字结点2,3,4,所以结点1的度为3. 2.树的度 树的度等于所有结点度中度最高的值.例如:上图中结点度最高为3,所以树的度为3. 3.叶子结点 叶子结点是度为0的结点即没有子结点的结点.例如:上图中3,5,6,7,9,10. 4.分支结点 分支结点是除了叶子结点,树中的其他所有结点.例如:上面树的分支结点为1,2,4,8. 5.内部结点 内部结点是除了根结点以及叶子结点或在分支结点的基础之上在去掉根结点.例如:上面树的内部

【算法导论】第十一课 扩充的数据结构、动态有序统计和区间树

本节课主要讲了如何构造自己想要的数据结构,或者扩充已有数据结构的功能,以实现想要的特定功能 比如设计一个动态结构,满足功能寻找第k大的数 其做法是维护每个结点的子结点个数来推导其秩,而不维护其秩,因为动态操作会使得其难以维护 红黑树的插入操作 1.树插入 2.rebalance 构造自己需要的扩充数据结构的基本流程 1.选择一个基本的数据结构 例如红黑树 2.决定要添加到结点的基本信息  例如实现查询第k大数功能,应添加的基本信息为所有子树结点之和,而非直接保存该结点键值的秩 3 维持 插入+旋

数据结构之——树与二叉树

树的基本概念: 树的概念是学习树的关键所在,掌握了树的基本概念,学会树与二叉树,so easy.我通过一棵树来了解树的基本概念,如下图 1.结点的度 结点的度是子结点的个数.例如:结点1有三个字结点2,3,4,所以结点1的度为3. 2.树的度 树的度等于所有结点度中度最高的值.例如:上图中结点度最高为3,所以树的度为3. 3.叶子结点 叶子结点是度为0的结点即没有子结点的结点.例如:上图中3,5,6,7,9,10. 4.分支结点 分支结点是除了叶子结点,树中的其他所有结点.例如:上面树的分支结点