本文最后更新于 2026-06-22T23:37:24+08:00
1. 树 定义:对一个节点而言,上层元素为直接前驱而且直接前驱唯一,下层元素为直接后继且直接后继可以有多个
根,子树
一个结点子树的根称为孩子节点,反之为父节点
父结点的父结点是祖父结点
根到某结点路径上所有结点(含根节点)都是祖先结点。反之都是祖先结点的子孙结点
同一父节点的结点互为兄弟结点
树中每个结点拥有孩子结点个数为结点的度,度为0是叶子结点,不为0是中间结点
树的度是每个结点度的最大值
根的层次数规定为1,其余结点层次数是父结点+1,所有结点层次数最大值是树的高度
若每个孩子节点被规定了顺序,称为有序数,否则是无序树
两棵以上的树是森林
父结点是孩子结点的直接前驱,孩子节点是父结点的直接后继。直接前驱唯一,直接后继可以有多个
2. 二叉树 2.1 二叉树的定义 二叉树和树是两种完全不同的结构,二叉树不是特殊的树。
二叉树允许空树存在,而树中结点个数至少是1,证明二叉树更多作为一种工具存在
二叉树左右孩子要明确指出是左还是右,即使只有一个孩子也要说明是左子还是右子。有序树中孩子只是排序了,没有左右之分
若k层二叉树每一层结点数量都达到最高值,则是满二叉树
若k层二叉树前k-1层是满的,第k层可能缺少一些结点,==但是缺少的结点是自右向左的==,称为==完全二叉树==
2.2 二叉树的性质
一棵非空二叉树的第i层上最多有$2^{i-1}$ 个结点。
高度为k的二叉树,最多有$2^k-1$个结点
对于一棵非空二叉树,叶子结点个数为$n_0$,度数为2的结点个数为$n_2$,有:$n_0=n_2+1$。
具有n个结点的完全二叉树高度为$k=[log_2n]+1$。
若将二叉树按层次遍历编号,i>1时,父节点编号为[i/2];若2i>n,编号为i的结点无左儿子,否则左儿子编号为2i;若2i+1>n,编号为i的结点无右儿子,否则右儿子编号为2i+1
2.3 二叉树的存储
数组存储,但是数组退化成一条链时浪费空间
链表存储:left、right、data
2.4 二叉树的遍历 2.4.1 前序遍历 根左右
递归算法:
1 2 3 4 5 6 7 8 void pre_order (node *t) { if (t!=NULL ){ cout<<t->data<<" " ; pre_order (t->left); pre_order (t->right); } }
非递归算法: 根进栈。 重复操作:出栈并访问,访问右子,如有右子,右子进栈,若有左子,左子进栈。
1 2 3 4 5 6 7 8 9 10 11 12 13 void pre_order (node *t) { stack<node*> s; s.push (t); while (!s.empty){ node *p=s.top (); s.pop (); cout<<p->data<<" " ; if (p->right!=NULL ) s.push (p->right); if (p->left!=NULL ) s.push (p->left); } }
时间复杂度为o(n)
2.4.2 中序遍历 左根右
递归算法:
1 2 3 4 5 6 7 void in_order (node *t) { if (t==NULL ) return ; pre_order (t->left); cout<<t->data<<" " ; pre_order (t->right); }
非递归算法: 引入flag,记录结点是第几次被访问 根进栈;初始flag均为0; 重复以下操作直至栈空:出栈,若flag=0;flag++,入栈,如有左子,左子入栈;若flag=1,访问,如有右子,入栈。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void in_order (node *t) { stack<node *> s; stack<int > flag; auto push_node=[&s;&flag](node *p,int f){ s.push (p); flag.push (f); } push_node (t,0 ); while (!s.empty ()){ node *p=s.top (); int f=flag.top (); s.pop (); flag.pop (); if (f==0 ){ push_node (p,1 ); if (p->left!=NULL ) push_node (p->left,0 ); } else { cout<<p->data<<" " ; if (p->right!=NULL ) push_node (p->right,0 ); } } }
时间复杂度为o(n)
2.4.3 后序遍历 左右根
递归算法:
1 2 3 4 5 6 7 void post_order (node *t) { if (t==NULL ) return ; post_order (t->left); post_order (t->right); cout<<t->data<<" " ; }
非递归算法: 若flag=0,flag++,该结点再次入栈,若有左子,左子也入栈;若flag=1,flag++,该结点再次入栈,若有右子,右子入栈;若flag=2,访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void post_order (node *t) { stack<node *>s; stack<int > flag; auto push_node=[&s;&flag](node *p,int f){ s.push (p); flag.push (f); } push_node (t,0 ); while (!s.empty ()){ node *p=s.top (); int f=flag.top (); s.pop (); flag.pop (); if (f==0 ){ push.node (p,1 ); if (p->left!=NULL ) push node (p->left,0 ) ; } else if (f==1 ){ push_node (p,2 ); if (p->right!=NULL ) push_node (p->right,0 ); } else cout<<p->data<<' ' ; } }
时间复杂度是o(n)
2.4.4 层次遍历 根节点进队 重复操作直至队空:队首结点出队访问;若有左子,左子进队;若有右子,右子进队
1 2 3 4 5 6 7 8 9 10 11 12 void BinaryTree levelOrder () { if (root==NULL ) return ; queue<node *>q; q.push (root); while (!q.empty ()){ int p=q.front (); q.pop (); cout<<p->data<<" " ; if (p->left!=NULL ) q.push (p->left); if (p->right!=NULL ) q.push (p->right) } }
时间复杂度是o(n)
2.5 二叉树类及操作实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 template <class T , T NULL_VALUE> BinaryTree<T,NULL_VALUE>::BinaryTree (const vector<T> &v){ if (v.empty ()){ root=NULL ; return ; } queue<Node*> q; root=new Node (v[0 ]); q.push (root); for (int i=1 ;i<v.size ();i+=2 ){ Node<T,NULL_VALUE> *p=q.front (); q.pop (); if (v[i]!=NULL_VALUE){ p->left=new Node <T,NULL_VALUE>(v[i]); q.push (p->left); } if (i+1 <v.size ()&&v[i+1 ]!=NULL_VALUE){ p->right=new Node <T,NULL_VALUE>(v[i+1 ]); q.push (p->right); } } }
类似层次遍历的思想,出队时两个孩子入队
size height
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 template <class T ,T NULL_VALUE>int BinaryTree<T,NULL_VALUE>::_size(Node<T,NULL_VALUE> *t){ if (t==NULL ) return 0 ; return 1 +_size(t->left)+_size(t->right); }template <class T ,T NULL_VALUE>int BinaryTree<T,NULL_VALUE>::_height(Node<t,NULL_VALUE> *t){ if (t==NULL ) return 0 ; int hl=height (t->left); int hr=height (t->right); return (hl>hr)?(hl+1 ):(hr+1 ); }template <class T , T NULL_VALUE> int BinaryTree<T, NULL_VALUE>::size (){ return _size(root); }template <class T ,T NULL_VALUE> int BinaryTree<T,NULL_VALUE>::height (){ return _height(root); }
删除一棵树
1 2 3 4 5 6 7 8 9 10 11 12 template <class T ,T NULL_VALUE>void BinaryTree<T, NULL_VLAUE>::_del_tree(Node<T,NULL_VALUE> *t){ if (t==NULL ) rerturn; _del_tree(t->left); _del_tree(t->right); delete t; }template <class T ,T NULL_VALUE> void BinaryTree <T,NULL_VALUE>::delTree (){ _del_tree(root); root=NULL ; }
3. 遍历确定二叉树
已知一棵完全二叉树的层次遍历,能唯一确定这棵完全二叉树
已知一棵满二叉树的前序、后序、中序遍历之一,可以唯一确定一棵满二叉树
已知一个一般二叉树的前序和中序遍历,可以唯一确定一棵二叉树
已知一个一般二叉树的后序和中序遍历,可以唯一确定一棵二叉树
已知一个一般二叉树的前序和后序遍历,不可以唯一确定一棵二叉树
代码实现:前序+中序 思想:由前序数组定出根结点值后,在中序数组找到根结点值所在的位置,由此找到了前序和中序序列中根的左子树下标范围,同样也找到了右子树的下标范围。接下来递归左右子树下标范围即可确定该二叉树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 template <class T , T NULL_VALUE>void BinaryTree<T, NULL_VALUE>::buildTreePreIn (const std::vector<T> &pre, const std::vector<T> &in) { if (pre.empty () || in.empty () || pre.size () != in.size ()) return ; unordered_set<T> pre_set (pre.begin(), pre.end()) ; unordered_set<T> in_set (in.begin(), in.end()) ; if (pre_set != in_set || pre_set.size () != pre.size ()) return ; function<Node<T, NULL_VALUE> *(int , int , int , int )> build =[&](int pre_l, int pre_r, int in_l, int in_r) -> Node<T, NULL_VALUE> * { if (pre_l > pre_r || in_l > in_r) return nullptr ; Node<T, NULL_VALUE> *root = new Node <T, NULL_VALUE>(pre[pre_l]); int i = in_l; while (i <= in_r && in[i] != pre[pre_l]) ++i; int left_size = i - in_l; root->left = build (pre_l + 1 , pre_l + left_size, in_l, i - 1 ); root->right = build (pre_l + left_size + 1 , pre_r, i + 1 , in_r); return root; }; delTree (); root = build (0 , pre.size () - 1 , 0 , in.size () - 1 ); }
4. 最优二叉树
加权路径长度(WPL):从根节点到各个叶子结点的路径长度×该叶子权值之和
最优二叉树:WPL最小,权值越大越靠近根
哈夫曼算法:1.若U中只有一个结点,操作结束,否则转向第2步。2.在集合中选取两个最小节点x,y 构造新的结点z,权值为x y权值之和,在集合U中删除结点x y并加入新结点z,再转向第一步
哈夫曼树一定是最优二叉树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class HuffmanNode { public : int data; int weight; HuffmanNode *left; HuffmanNode *right; HuffmanNode (x,y,*a,*b):data (x),weight (y),left (a),right (b){}; };HuffmanNode buildHuffmanTree (vector<HuffmanNode *> &nodes) { while (nodes.size ()>1 ){ auto [min1,min2]=findMinTwoNodes (nodes); HuffmanNode *new_node=new HuffmanNode ('\0' ,min->weight+min->weight,min1,min2); nodes.erase (remove (nodes.begin (),nodes.end (),min1),nodes.end ()); nodes.erase (remove (nodes.begin (),nodes.end (),min2),nodes.end ()); nodes.push_back (new_node); } return nodes.empty ()? NULL :nodes[0 ]; }static pair<HuffmanNode *,HuffmanNode *> findMinTwoNodes (vector<HuffmanNode *> &nodes) { HuffmanNode *min1=NULL ; HuffmanNode *min2=NULL ; int min_weight1=INT_MAX; int min_weight2=INT_MAX; for (auto node:nodes){ if (node->weight<min_weight1){ min_weight2=min_weight1; min2=min1; min_weight1=node->weight; min1=node; } else if (node->weight<min_weight2){ min_weight2=node->weight; min2=node; } return {min1,min2}; } }
时间复杂度$o(n^2)$,但学了堆以后,找到权值最小的两个元素的时间复杂度降至o(logn),时间复杂度降至o(nlogn)
哈夫曼编码:从根开始遍历。向左则字符串加0,向右加1,直至遍历到叶子结点。 ==可用的编码只能出现在叶子结点上!!!==
1 2 3 4 5 6 7 8 9 void printHuffmanCodes (HuffmanNode *root,string code) { if (!root) return ; if (!root->left && !root->right){ cout<<root->data<<" " <<code<<endl; } printHuffmanCodes (root->left,code+'0' ); printHuffmanCodes (root->right,code+'1' ); }
时间复杂度为o(n)
5. 树和森林
前序遍历:放问完根节点再从左至右前序遍历子树
后序遍历:先从左至右逐个后序遍历所有子树再访问根
根有多个孩子,故无法定义中序遍历
前序遍历:访问第一棵树的根结点,前序遍历第一棵树中所有根结点的所有子树形成的森林,从左至右同样方式依次访问所有的树
后序遍历:中序遍历第一棵树中根的子树形成的森林,访问第一棵树的根节点,从左至右依次访问所有树
森林的先根遍历就是对应二叉树的先序遍历,==后根遍历是对应二叉树的中序遍历==
在内存中有一个用二叉树表示的一棵树
如何求树的高度?二叉树的前序遍历,访问一个结点时,左子的层次为父结点层次加一,右子的层次和父结点的一样的方法来处理
求森林中树有几棵?可以在二叉树中顺着根,一路右子计数过去,便可解决
==已知一棵树的前序遍历和后序遍历,可以唯一决定一棵树。== 一棵树的前序遍历序列即其对应孩子兄弟二叉树的前序遍历,一棵树的后序遍历序列即其对应孩子兄弟二叉树的中序遍历,而已知二叉树的前序和中序遍历可以唯一确定这棵二叉树,根据这棵二叉树又能唯一对应一棵树。
6. 优先级队列 6.1 基本的优先级队列(基于线性表) 结点之间的关系是由结点的优先级决定的,而不是由入队的先后次序决定。优先级高的先出队,优先级低的后出队。
处理优先级的两种方法:
入队时,按照优先级在队列中寻找合适的位置,将新入队的元素插入在此位置;出队操作的实现保持不变。入队操作的时间复杂度是O(N),出队操作的时间复杂度是O(1)
入队操作的实现保持不变,将新入队的元素放在队列尾;但出队时,在整个队列中查找优先级最高的元素,让它出队。入队操作的时间复杂度是O(1),出队操作的时间复杂度是O(N)。
6.2 二叉堆(基于树的优先级队列)
最小化堆:父结点小于子结点
最大化堆:父结点大于子结点
存储: 由于==二叉堆是完全二叉树==,可以采用顺序存储。二叉堆的有序性可以很容易地通过下标来反映。一般为方便计算,==根结点的下标为1==。
1 2 3 4 5 6 7 8 9 10 11 12 13 template <typename T> class PriorityQueue {private : std::vector<T> data = std::vector <T>(1 ); public : PriorityQueue () = default ; PriorityQueue (const std::vector<T> &elements); void push (const T &value) ; T pop () ; T top () const ; bool empty () const ; size_t size () const ; };
push操作: 在具有最大序号的元素之后插入新的元素或结点,如果新元素放入后,没有违反堆的有序性,那么操作结束。否则,让该节点向父节点移动,直到满足有序性或到达根节点。新节点的向上移动称为向上过滤。
1 2 3 4 5 6 7 8 9 10 void push (const T &value) { data.push_back (value); heapify_up (data.size () - 1 ); }void heapify_up (int index) { while (index > 1 && data[index] < data[parent (index)]) { std::swap (data[index], data[parent (index)]); index = parent (index); } }
时间复杂度:最坏情况是O(logN),即完全二叉树的高度,平均情况,过滤会提前结束
pop操作: 当最小元素被删除时,在根上出现了一个空结点,放入最后一项,向下过滤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 T pop () { if (data.size () <= 1 ) { throw std::out_of_range ("PriorityQueue is empty" ); } T maxValue = data[1 ]; data[1 ] = data.back (); data.pop_back (); if (data.size () > 1 ) { heapify_down (1 ); } return maxValue; }void heapify_down (int index) { int size = data.size (); while ((index *2 ) < size) { int child = index*2 ; if (child + 1 < size && data[child + 1 ] < data[child]) { child++; } if (data[index] <= data[child]) { break ; } std::swap (data[index], data[child]); index = child; } }
时间复杂度o(logn)
建堆:自下往上调整每一个子堆。从最大的非叶索引开始,每一个向下过滤
1 2 3 4 5 6 PriorityQueue (const std::vector<T> &elements) { for (const auto &element : elements) data.push_back (element); for (int i = data.size () / 2 ; i > 0 ; --i) heapify_down (i); }
建堆时间复杂度o(n)