本文使用 Zhihu On VSCode 创作并发布
- 数据结构(邓俊辉/严蔚敏)的学习
- 第一章 绪论
- 第二章 向量
- 第三章 列表
- C语言实现链表
- 内核链表/通用链表
- 第四章 栈与队列
- 波兰式转逆波兰式
- 使用顺序表实现循环队列
- N皇后
- 第五章 二叉树
- 先中后序遍历的非递归写法
- 二叉树性质(根节点层数为1)
- 由n个节点可以构造出多少种不同的二叉树?
- 满二叉树(Full Binary Tree / Perfect Binary Tree)
- 完全二叉树(Complete Binary Tree)
- 二叉树的重构(要求遍历序列无重复数字)
- Huffman编码树的简单实现,对文本进行编码与解码
- 中序线索二叉树(Threaded Binary Tree)的简单实现
- 树和森林
- 其他
- 最后,如何去储存一棵二叉树呢? LeetCode297. 二叉树的序列化与反序列化
- 第六章 图
- 最小生成树(本质都是贪心)
- 第七章 查找 (严蔚敏版)
- 二分(折半)查找
- 分块查找
- 二叉搜索树
- 平衡二叉树
可能更好的阅读体验与更快的更新
第一章 绪论
-
为
n
二进制展开的位数 , 同理为十进制 数n
的位数. 实际上,由基本的数论知识不难验证 , 任意整数的平方关于5整除之后的余数,断乎不可能是2(或3).
- 由
,且,故
- 由
经过条件判断
(12 < n) && (sqrt(f) * sqrt(f) == f)
之后的“递归调用”,依然绝对不可能被执行 . 实际上在Fibonacci数中,只有fib(0)、fib(1)、fib(2)、fib(12) 是平方数,fib(n > 12)必然都不是。- 对于整数或者浮点数 f ,只有 f 为平方数时 ,
sqrt(f) * sqrt(f) == f
才为真
- 对于整数或者浮点数 f ,只有 f 为平方数时 ,
因为这篇文章,简单学了latex,写出来的公式真漂亮.
第二章 向量
本书应该是默认使用了
#include <utility>; using namespace std;
,因为代码中使用了swap
;同时rand()
包含在头文件<cstdlib>
中Visual studio code
对c++的模板没有语法检查 原因 , 对于使用了模板的C++代码推荐使用Visual Studio
,在指定了模板的类型后有语法检查.p37使用的置乱算法是
高纳德置乱算法(Knuth-Fisher-Yates algorithm)
我想到的是
Naive algorithm
``` for(int i = 0 ;i < n ;i++) { swap(a[i],a[rand() % n]); } ```
这种写法得到的所有可能结果有
种,而不是 $ n! $ 种,而且不可能是的整数倍。Reference比如说
arr = {1,2,3}
,正确的结果应该有种可能,而这种写法总共有种可能结果。因为 27 不能被 6 整除,所以一定有某些情况被「偏袒」了,也就是说某些情况出现的概率会大一些,所以这种打乱结果不算「真的乱」。
二分搜索 根据
std::lower_bound()
去实现 , 这个 回答 我认为很好的解释了思想.遍历操作
Vector<T>::traverse()
使用了函数对象 . 我的理解是函数对象就是重载了operator()的类
书中
binSearch() , fibSearch()
声明为static
函数,即为全局静态函数,其意义在于 作用域局限于一个源文件内,静态函数不能被其它文件所用.Vector代码实现
C语言实现线性表
第三章 列表
在
C++11
标准之后,可以使用template <typename T> using ListNodePosi = ListNode<T>*;
代替书中的#define ListNodePosi(T) ListNode<T>*
, 但是请记得前置声明类.在本书列表的实现中,我发现了一些错误 我购买书的印次是2019年10月第17次印刷,书中我所说部分的代码是错误的,邓老师主页的代码是正确的
将临时变量传递给了非const引用
,函数first()
返回的是值为header->succ
临时变量,在重载的merge(...)
作为了类型为ListNodePosi(T)&
的参数#defind ListNodePosi(T) ListNode<T> * ListNodePosi(T) first() const { return header->succ; } void merge(ListNodePosi(T)& ,int ,List<T>& ,ListNodePosi(T) ,int){ //实现省略... } void merge(List<T>& L) { merge(first(),size,L,L.first(),L._size); }
第二个其实也是类型的错误,很明显了
T& List<T>::operator[] (Rank r) const { //... } void copyNodes(ListNodePosi(T) p,int n){ // ... } List<T>::List(List<T> const& L,int r,int n) { copyNodes(L[r],n); }
双向链表代码实现
C语言实现链表
- 简单的双向链表实现,同时也实现了归并排序.
- 代码实现
- 今天在用C语言写链表的归并排序时,在同一个链表中
merge
其中两个有序序列时,传进去的指针所指向的地方可能会被free
掉,从而影响后续的归并.需要保存前一个节点,然后通过pred->succ
找到它自己. 看懂了代码,理解了思想,不等于会写了.所有东西你都要自己实现一遍,我觉得很好的例子就是二分搜索,思想很简单,但你要bug free
实现一个,还是需要自己去打磨熟悉,这一过程我感觉比你去理解他更费功夫. 数据结构的学习,我使用邓老师这个教材去学,但每一部分我都要尽量用C语言去实现一遍.
内核链表/通用链表
- Linux内核的实现,大家可以根据
Github@Akagi201
的移植,去学习 Port linux kernel list.h to userspace. - Linux kernel list 的讲解 .
- 自己的实现 与 测试 ,仅实现了双向链表部分.
- 写法较传统链表没有太多区别,但思想很精妙,尤其是
container_of
与offsetof
非常好得体现了C语言对内存的操作. - 关于
offsetof
宏解空指针被大多标准明确定义为未定义行为,但对于编译器,它可以
define the behavior
. 在GNU中,typeid
就依赖于解空指针,可见解空指针不一定是UB. 更多特别的,在C++中
-
offsetof
不能以标准 C++ 实现,并要求编译器支持: GCC 、 LLVMoffsetof (type, member)
:若type
不是简旧数据类型 (PODType) (C++11 前)标准布局类型 (C++11 起),则行为未定义 (C++17 前)C++17 : Use of the
offsetof
macro with a type other than a standard-layout class is conditionally-supported. POD类型 和 标准布局类型
-
offsetof
宏自己实现仅作学习用途 , 因此#include <stddef.h>
依靠编译器实现才是正道.
第四章 栈与队列
栈的实现
队列的实现
波兰式转逆波兰式
- Code
//b优先级是否低于a bool isPriority(char a, char b) { if ((a == '-' or a == '+') and (b == '*' or b == '/')) return false; return true; } /* 样例: infixToPostfix("96 + ( 5 * 6 - 3 ! ) + 10 / 2") == "96 5 6 * 3 ! - + 10 2 / + " */ string infixToPostfix(const string& source) { string res; stack<char> s; for (int i = 0; i < source.size(); i++) { switch (source[i]) { case ' ': break; case '(': s.push('('); break; case ')': while (s.top() != '(') { char op = s.top(); res.push_back(op); res.push_back(' '); s.pop(); } s.pop(); break; case '!': res.push_back('!'), res.push_back(' '); break; default: if (isdigit(source[i])) { res.push_back(source[i]); if (i + 1 > source.size() - 1 or !isdigit(source[i + 1])) res.push_back(' '); } else { while (!s.empty() and s.top() != '(' and isPriority(s.top(), source[i])) { char op = s.top(); s.pop(); res.push_back(op); res.push_back(' '); } s.push(source[i]); } } } while (!s.empty()) { res.push_back(s.top()); res.push_back(' '); s.pop(); } return res; }
使用顺序表实现循环队列
- 判断队空/队满
- 浪费一个元素的存储空间:front指向队首的实际位置,rear指向实际位置的下一个位置,
front==rear
时为空,(rear+1) % m == front
时为满 - 使用标记tag,front指向队首实际位置,rear指向队尾实际位置的下一个位置,入队时
tag=1
,出队时tag=0
,当front == rear && tag == 0
时队空,当front == rear && tag == 1
时队满 - 使用
front
,rear
,length
作为判断队空队满的标志,这样就不用浪费一个元素的存储空间了,和tag
的作用是一样的,front
指向队首实际位置,rear
指向队尾实际位置 ,length == MAXSIZE
队满
- 浪费一个元素的存储空间:front指向队首的实际位置,rear指向实际位置的下一个位置,
- 队列大小
(rear - head + MAXSIZE) % MAXSIZE
N皇后
Code
// https://vjudge.net/problem/HDU-2553 #include <bits/stdc++.h> using namespace std; int n; int column[11]; int cnt = 0; void dfs(int row) { if (row > n) { cnt++; return; } for (int i = 1; i <= n; i++) { bool flag = true; for (int j = 1; j < row; j++) { if (i == column[j] || column[j] - j == i - row || column[j] + j == i + row) { flag = false; break; } } if (flag) { column[row] = i; dfs(row + 1); } } } int main(void) { while (cin >> n && n != 0) { memset(column, 0, sizeof(column)); cnt = 0; dfs(1); cout << cnt << endl; } return 0; }
第五章 二叉树
先中后序遍历的非递归写法
先中后序遍历的非递归写法(称之为迭代法可能不太严谨)
- 书中遍历的迭代写法中除了中序遍历利用节点的直接后继达到了O(1)的空间复杂度,其余均需要利用辅助栈,因此我个人认为在使用了栈的前提下,老师书中的写法理解起来并没有那么轻松,反倒是利用空指针作为根节点的标记的写法理解起来容易,整体想法与递归将近.
二叉树性质(根节点层数为1)
- 二叉树性质
- 二叉树第
层上至多有个节点.
- 深度为
的二叉树至多有个节点,至少有个节点 .
- 对于任何一棵二叉树,若其叶子节点数为
,度为的节点数为,则
- 包含
个节点的二叉树,其高度至少为,即完全二叉树,至多为$n $,此时为一棵斜树.
- 二叉树第
由n个节点可以构造出多少种不同的二叉树?
满二叉树(Full Binary Tree / Perfect Binary Tree)
- 满二叉树
- 所有internal node都有兩個subtree
- 所有leaf node具有相同的level(或相同的height)
- 按照层序遍历从上到下,从左到右编号,
完全二叉树(Complete Binary Tree)
- 完全二叉树
- 对于具有n个结点的二叉树按层序编号,如果每个结点的编号与同样深度的满二叉树中对应编号的结点在二叉树中位置完全相同,则该二叉树称为完全二叉树。
- 编号规律与满二叉树一致
- 具有
个节点的完全二叉树的高度为
-
- 满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。
- 叶子结点只出现在最下两层,最下层的叶子一定集中在左部连续位置,倒数二层若有叶子节点一定集中在右部连续位置
- 如果结点度为1,则该结点只有左孩子
二叉树的重构(要求遍历序列无重复数字)
对于任何二叉树,知道其
[先序|后续]与中序
可唯一确定一棵二叉树对于真二叉树,知道其
先序与后序
可唯一确定一棵真二叉树(真二叉树是各个节点的子树个数均为偶数()的二叉树)普通二叉树与真二叉树重构 代码实现
Huffman编码树的简单实现,对文本进行编码与解码
Huffman编码树的简单实现,对文本进行编码与解码
- 正确性验证
Image
- 正确性验证
中序线索二叉树(Threaded Binary Tree)的简单实现
中序线索二叉树(Threaded Binary Tree)的简单实现
- #图解 数据结构:轻松搞定线索二叉树 - 知乎 (zhihu.com)
- 线索二叉树是将每个节点的空指针域(即左/右孩子为空)利用起来,将原本为空的指针指向其在某种遍历序列中的直接前继或者直接后继,用
L/RTag
去区别指针指向的是孩子还是其前继/后继. - 在对二叉树中序线索化的函数
inThreading()
中- 基本逻辑就是递归版的中序遍历,在遇到当前节点的
lchild
为空时,修改lchild
指针为当前节点的直接前继,更改LTag
为true
. (当前节点的直接前继用全局变量pre
指针表示,初始为NULL
,在遍历过程中对其进行修改.pre
始终为刚刚访问完的节点) .如果pre
的右孩子为空,修改pre
的rchild
指向他的直接后继,也就是当前节点. - 因为中序遍历的默认顺序是左根右,因此对于当前节点其前继是已知的即
pre
,所以我们修改他的lchild
;对于pre
节点,其后继是已知的,所以我们修改pre->rchild
. - 在完成中序遍历线索化之后,我们可以知道对于整个二叉树,只有中序序列的第一个节点的
lchild
与最后一个节点的rchild
指针为空.当L/RTag == true
时,该节点无左/右孩子.
- 基本逻辑就是递归版的中序遍历,在遇到当前节点的
- 在对中序线索二叉树的遍历函数
inOrderThread()
中- 首先通过
firstAtInorder()
函数找到该二叉树中序序列的第一个节点.第一个节点为该树最左侧通路的最深的那个节点(邓俊辉 DSA p128),也就是树最左侧那个节点. 只要沿着节点,一路走左孩子,直到LTag == true
即可. - 接着找当前节点的后继节点
nextAtInorder()
.如果当前节点没有右孩子即RTag == true
,则其后继为lchild
;如果有,则为当前节点的右子树中的最左侧的节点,使用firstAtInorder()
即可. - 对于左根右的中序遍历,更改空的
lchild
指向他的直接前继并没有什么用. - 强烈建议这部分和邓老师的迭代版遍历那部分一起看,邓老师中序遍历空间O(1)的写法也提到直接后继,书上的图也帮助理解.
- 首先通过
- 我讨厌线索二叉树
树和森林
- 树和森林
树的存储方式
- 双亲表示法 : 用一组连续空间储存
pair<data,parent's index>
. 双亲这翻译太误导了 - 孩子表示法 : 孩子链表 / 带双亲的孩子链表
- 孩子兄弟表示法 : 又称二叉树表示法/二叉链表表示法,指针指向第一个孩子节点与下一个兄弟节点. 当说到二叉链表时,我也不知道是指孩子孩子还是孩子兄弟 . 多叉树可等价为二叉树就是用这种表示方法.
- 双亲表示法 : 用一组连续空间储存
森林与二叉树转换
- 由孩子兄弟二叉链表表示法我们知道任何一颗树的根节点的右子树为空,我们可以认为森林中第二棵树的根节点是第一棵树的根节点的兄弟.
其他
一棵二叉树的先序遍历序列与后序遍历序列正好相反,则该二叉树为高度等于其结点个数的二叉树,即任意结点只有左孩子或只有右孩子,先序序列即从上向下的层序,后序序列即从下向上的层序.
二叉树的储存结构
- 顺序存储结构
- 对于完全二叉树,将编号为
i
的节点一维数组中下标为i-1
的位置 - 对于一般二叉树,将其节点与完全二叉树上的节点对应. 最坏情况下,有
n
个节点的右斜树,需要的空间.
- 对于完全二叉树,将编号为
- 链式存储结构
- 二叉链表
- 三叉链表 ...
- 顺序存储结构
最后,如何去储存一棵二叉树呢? LeetCode297. 二叉树的序列化与反序列化
- 最后,如何去储存一棵二叉树呢? LeetCode297. 二叉树的序列化与反序列化
层序遍历,序列中 每个有效节点后面的第二/三个节点分别代表其左右孩子.
-
/* struct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNode(int x) : val(x), left(NULL), right(NULL) { } }; */ class Codec { public: // Encodes a tree to a single string. string serialize(TreeNode* root) { queue<TreeNode*> q; string res; q.push(root); while (not q.empty()) { TreeNode* now = q.front(); q.pop(); if (now) { q.push(now->left); q.push(now->right); res += to_string(now->val); res.push_back(','); } else res += "null,"; } return res; } // Decodes your encoded data to tree. TreeNode* deserialize(string data) { size_t l = 0, r = data.find(','); string sub = data.substr(l, r - l); if (sub == "null") return nullptr; int num = stoi(sub); TreeNode* root = new TreeNode(num); queue<TreeNode*> q; q.push(root); while (not q.empty()) { TreeNode* now = q.front(); q.pop(); l = r + 1; r = data.find(',', l); sub = data.substr(l, r - l); if (sub != "null") { num = stoi(sub); now->left = new TreeNode(num); q.push(now->left); } l = r + 1; r = data.find(',', l); sub = data.substr(l, r - l); if (sub != "null") { num = stoi(sub); now->right = new TreeNode(num); q.push(now->right); } } return root; } };
第六章 图
- 强连通图 : 在有向图G中,如果两个顶点间至少存在一条互相可达路径,称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。
- 强连通分量 : 非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected components)。
- 十字链表 与 邻接多重表
- 深度优先生成树 与 广度优先生成树
最小生成树(本质都是贪心)
- 最小生成树
- Prim算法
- 基于邻接矩阵的图 时间复杂度
- 因为时间复杂度与
无关,适用于求稠密网的最小生成树
- 基于邻接表的图 + 二叉堆优化 时间复杂度
- 基于邻接矩阵的图 时间复杂度
- Kruskal算法 使用堆排序时间复杂度
- 更适用于求稀疏网的最小生成树
- Prim算法
- 最短路
- Dijkstra
- Floyd
- 关键路径 这东西不就是个最长路嘛?
第七章 查找 (严蔚敏版)
- 顺序查找
二分(折半)查找
- 二分搜索
-
- lower_bound,upper_bound,binarySearch代码实现 , 可参照CppReference中的实现
- 二分想着很简单,但要是Bug Free的实现没那么简单
lower/upper_bound
这俩兄弟容易把人写懵 最开始end
指向数组尾的下一个元素,为了在没找到tar
时返回数组尾下一个位置.当*mid
元素非法时,修改begin
.反之,修改end
.直到begin
和end
重合,即为目标位置. 这里的非法指我们找的元素一定不在mid
之前,我们就可以把mid
之前的元素直接排除掉,令begin= mid + 1
.当*mid
元素合法时,即符合我们的搜索条件,因为我们要找到时第一个符合条件位置,因此mid
之后的元素都不用考虑了,故修改end = mid
. 记住三点, 1. 最开始end
指向数组尾下一个元素 2. 始终将target
控制在[begin
,end
]之内 3.我们要找的的第一个符合条件的元素.以此去理解end
的变化 binary_search
实现只需要调用lower_bound
,判断返回指针是否是合法地址&&
指向元素是否等于target
即可.
- 二分想着很简单,但要是Bug Free的实现没那么简单
-
分块查找
- 分块查找
- 要求表本身分块有序,即下一个子表中的最小值大于当前子表最大值.
- 由此建立索引表,要对索引表进行排序.
二叉搜索树
- 二叉搜索树
- https://www.deltablog.xyz/posts/binarysearchtree/
-
最坏情况下与顺序查找相同.
- 节点的删除
- 左子树上的值小于根节点,右子树的值大于根节点
- 当删除节点左右子树都存在时,找其左子树中最大的节点或者右子树中最小的节点代替当前节点即可.
- 代码实现 ... 写的一点也不好,罗里吧嗦...但至少能跑了,没发现bug
平衡二叉树
- Something