2021
年秋天我再次作为一个刚踏进带学校园的人出现。然而还是要面临一样的东西,我也没什么基础可言,仰望各位大佬,自己一无所得。除了会给
VSCode 配一个 C++
环境以外纯纯的黔驴技穷。想着去学校天天抄报告之前学点东西吧,于是下面的一系列笔记就这样诞生了。很简单,没啥东西,能满足
114514
本带学的考试及格,就是满足一下自己作为「正经人」写日记的感受而已,凑合看吧。
「re:
从零开始的带学数据结构①」几种基础的树的理解
二叉树的非递归遍历
深度优先搜索
先从递归遍历的方向去理解。
实际上递归操作本质上是栈的实现。函数调用的过程具有栈的形式,也就是先进后出。
中序遍历:每次在 pop
前后对元素进行操作。中序遍历会在第二次见到元素的时候进行操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Trav(节点) { while(节点、栈非空) while(节点非空) 节点压栈; 左移; 栈顶元素赋值给这个节点,折返; 栈 弹出栈顶元素; 节点右移,探索右部路径; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| vector<int>Trav(TreeNode *Node) { vector<int> Result; stack<TreeNode*> S; while(!s.empty() || Node == NULL) { while(Node) { S.push(Node); Node = Node->left; } Node = S.top(); S.pop(); Result.push_back(Node->value); Node = Node->right; } return Result; }
|
对二叉搜索树相关操作的理解
对于二叉搜索树,满足性质:
- 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
- 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
- 任意节点的左、右子树也分别为二叉搜索树;
于是,可以二分地去进行查找,只要待定值大,就去右边;小就去左边。可以递归的实现为:
1 2 3 4 5 6 7 8 9 10 11 12 13
| TreeNode *Find(T X, TreeNode* BSTNode) { if(!BSTNode) return NULL; if(X > BSTNode->Data) return Find(X, BSTNode->Right); else if(X < BSTNode->Data) return Find(X, BSTNode->Left); else return BSTNode; }
|
由上性质易知,最小的节点是最左侧的节点;反之亦然。
所以查找最大和最小的算法,可以藉由递归,不断地向左和向右来实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| TreeNode *FindMin(TreeNode *BSTNode) { if(BSTNode) while(BSTNode->Left) BSTNode = BSTNode->Left;
return BSTNode; }
TreeNode *FindMax(TreeNode *BSTNode) { if(!BSTNode) return NULL; else if(!BSTNode->Right) return BSTNode; else return FindMax(BSTNode->Right); }
|
在整棵树上寻找位置插入,便可以利用上面的规律:
- 考察待插入节点值的大小,削减规模到 在左 /
右子树上寻找位置插入。
- 在削减规模的过程中,通过递归的返回值,把插入节点以后的变化了的左子树再赋回原来的左子树上。
- 如果寻找的位置已经越出了树的内存地址(NULL),那么就新建一个节点。把它连接到上次的左右子树上面。这个时候,代表已经走到了树的叶节点的下一个位置,就可以在它后面插入了。代码实现如下:
- 新插入节点总是叶子节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| TreeNode *BSTInsert(TreeNode *BSTHead, T Value) { if(!BSTHead){ BSTHead = new TreeNode; BSTHead->Data = Value; BSTHead->Left = BSTHead->Right = NULL; } else{ if(Value < BSTHead->Data) BSTHead->Left = BSTInsert(BSTHead->Left, Value); else if(Value > BSTHead->Data) BSTHead->Right = BSTInsert(BSTHead->Right, Value); } return BSTHead; }
|
树的删除也是类似的道理。由二叉搜索树的性质易知,被删除节点的右子树的最小值没有左子树,可以直接删除。
- 先查找待删除结点的位置。削减规模。
- 找到待删除结点的右子树的最小值,把它赋给待删除结点的位置,节点删除完毕。
- 这个时候去清理冗余的、位于右子树最小值位置的节点。递归的进行这一步骤,它的节点数一定小于两个。
- 对于只有一个子树或者没有的树,可以直接把下面接上来。不需要进行 2、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 31 32 33 34 35 36 37 38 39 40 41
| TreeNode *BSTDelete(TreeNode *BST, T X) { TreeNode *TempNode = new TreeNode;
if(!BST) return NULL; else if(X < BST->Data) BST->Left = BSTDelete(BST->Left, X);
else if(X > BST->Data) BST->Right = BSTDelete(BST->Right, X); else { if(BST->Left && BST->Right) { TempNode = FindMin(BST->Right); BST->Data = TempNode->Data; BST->Right = BSTDelete(BST->Right, BST->Data); }
else { TempNode = BST;
if(!BST->Left) BST = BST->Right; else if(!BST->Right) BST = BST->Left;
delete TempNode; } }
return BST; }
|
堆的实现及其操作理解
这里的堆满足以下性质:
完全二叉树。每一个根节点都比两个子节点要大 / 小。
使用静态数组来构成堆。从 a[1]
开始逐个导入元素,完全二叉树一定满足: \[
ChildPos / 2 = ParentPos
\]
从父节点逐渐向下的序列一定满足单调性。
于是,便可以这样实现插入节点:
1 2 3 4 5 6 7 8
| Heap->Size++; int i = Heap->Size; while(Heap->Data[i/2] < Value) { Heap->Data[i] = Heap->Data[i/2]; i = i / 2; } Heap->Data[i] = Value;
|
对于删除节点,默认只会删除优先级 /
值最高的节点,也就是根节点。可以
- 把最末位的元素列出来,假设它可以放在头上。这一层暂定为 Parent。
- 比对根节点左右子树大小,选出更大的子树来。
- 如果最末位的元素已经比它大,就意味着不需要调整了。否则把子树的值赋给根节点本身。
- 将 Child 位置赋给 Parent。向下一层继续比对。
- 各层调整过后,最末尾元素归位。
可以理解成:最末位元素锚定的 Parent 位置元素。和更大的 Child
元素之间相互比较,更大的元素进入 Parent 位置,而更小的则通过步骤 4
保留在 Child
位置上。当这最末尾元素比左右子树都大或者不再有元素,便结束循环。
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
| T Remove(HeapContainer *H) { typedef uint32_t Position; Position Parent, Child;
if (H->Size == 0) return; T MaxItem = H->Data[1]; T TempElement;
TempElement = H->Data[H->Size]; H->Size--;
for(Parent = 1; Parent * 2 <= H->Size; Parent = Child){ Child = Parent * 2; if( (Child!= H->Size) && (H->Data[Child] < H->Data[Child+1]) ) Child++; if(TempElement >= H->Data[Child]) break; else H->Data[Parent] = H->Data[Child]; } H->Data[Parent] = TempElement; return MaxItem; }
|
堆的下滤(堆排序优化,时间复杂度可以达到 O(N))
Huffman 编码及其证明
给定一组符号及其对应的权重值。欲知一组二元的前置码,其加权的路径长为最短。
\[
L(C) = \sum_{i=1}^nw_i \times length(c_i)
\] 其中 w 表示权重;length
表示距离根节点的路径,也就是这样的路径长度。
Huffman 提出的、利用贪心算法原理实现的编码流程如下:
- 将不同字符分别建立成树的节点,包含权重、左右子树(暂时为
null)。
- 对节点按照权重排序。
- 挑出 2
个权重最小的节点,进行合并。这棵树的权重便是两个节点之和。把它再插入到原来的序列中。
- 重复过程 3,直到所有的节点都被归并为止。这样就生成了一棵 Huffman
树。