《大话数据结构》学习笔记(一)
前言
算法时间复杂度
常见的时间复杂度所耗费的时间从小到大依次是:
O
(
1
)
<
O
(
l
o
g
n
)
<
O
(
n
)
<
O
(
n
l
o
g
n
)
<
O
(
n
2
)
<
O
(
n
3
)
<
O
(
2
n
)
<
O
(
n
!
)
<
O
(
n
n
)
.
O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n).
O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn).
栈
实例
CCF 201903-2.二十四点
思路:使用栈,将数字压入栈中,遇到加号直接后移,减号就用改数字乘以-1压入栈中;当碰到乘法(除法)运算符的时候先让栈顶的元素出栈并与后面的数字相乘(相除),将得到的数字压入栈中;最后通过求栈中元素和得到结果。
//sample
/*
10
9+3+4x3
5+4x5x5
7-9-9+8
5x6/5x4
3+5+7+9
1x1+9-9
1x9-5/9
8/5+6x9
6x7-3x6
6x4+4/5
*/
//#include <bits/stdc++.h>
#include <iostream>
#include <stack>
using namespace std;
int a[100100];
stack<int> num;
int main()
{
std::ios::sync_with_stdio(false);
int n;
char str[14];
cin >> n;
while (n--){
cin >> str;
while (!num.empty())num.pop();
int i = 0;
while (i < strlen(str)){
if (isdigit(str[i])){
num.push(str[i] - '0');
i++;
}
else{
if (str[i] == '+'){
i++;
}
else if (str[i] == '-'){
i++;
num.push((str[i] - '0') * -1);
i++;
}
else if (str[i] == 'x'){
int x = num.top();
num.pop();
i++;
num.push((str[i] - '0') * x);
i++;
}
else if (str[i] == '/'){
int y = num.top();
num.pop();
i++;
num.push(y / (str[i] - '0'));
i++;
}
}
}
int sum = 0;
while (!num.empty()) {
sum += num.top();
num.pop();
}
if (sum == 24){
cout << "Yes" << endl;
}
else{
cout << "No" << endl;
}
}
return 0;
}
LeetCode 20.有效的括号「简单」
题目描述:
给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
1.左括号必须用相同类型的右括号闭合。
2.左括号必须以正确的顺序闭合。
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
示例 4:
输入:s = "([)]"
输出:false
示例 5:
输入:s = "{[]}"
输出:true
提示:
1 <= s.length <= 10^4
s 仅由括号 '()[]{}' 组成
通过次数604,540,提交次数1,374,974
#include <stack>
#include <string>
class Solution {
public:
bool isValid(string s) {
stack<char> stk;
int i=0;
while(i<s.length()){
if(s[i]=='(' || s[i]=='[' || s[i]=='{'){
stk.push(s[i]);
}
if((s[i]==')' || s[i]==']' || s[i]=='}') && stk.empty()){
return 0;
}
else if(s[i]==')'){
if(stk.top()=='('){
stk.pop();
}
else{
return 0;
}
}
else if(s[i]==']'){
if(stk.top()=='['){
stk.pop();
}
else{
return 0;
}
}
else if(s[i]=='}'){
if(stk.top()=='{'){
stk.pop();
}
else{
return 0;
}
}
i++;
}
if(stk.empty()){
return 1;
}
else{
return 0;
}
}
};
串
KMP
编号 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
S | a | b | c | a | c |
next | 0 | 1 | 1 | 1 | 2 |
next数组求解方法
void getNext(String T,int next[]){
int i=1,j=0;
next[1]=0;
while(i<T.length){
if(j==0 || T.ch[i]==T.ch[j]){
++i; ++j; next[i]=j;
}
else{
j=next[j];
}
}
}
KMP匹配算法
int indexKMP(String S,String T,int next[],int pos){
int i=pos,j=1;
while(i<=S.length && j<=T.length){
if(j==0 || S.ch[i]==T.ch[j]){
++i; ++j;
}
else{
j=next[j];
}
}
if(j>T.length){
return i-T.length;
}
else{
return 0;
}
}
KMP算法详解:点击查看
完整代码
class Solution {
public:
void getNext(int* next, const string& s) {
int j = -1;
next[0] = j;
for(int i = 1; i < s.size(); i++) { // 注意i从1开始
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
j = next[j]; // 向前回溯
}
if (s[i] == s[j + 1]) { // 找到相同的前后缀
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
}
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = -1; // // 因为next数组里记录的起始位置为-1
for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始
while(j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配
j = next[j]; // j 寻找之前匹配的位置
}
if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动
j++; // i的增加在for循环里
}
if (j == (needle.size() - 1) ) { // 文本串s里出现了模式串t
return (i - needle.size() + 1);
}
}
return -1;
}
};
实例
LeeCode 28.实现strStr()「简单」
实现 strStr() 函数。
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。
说明:
当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。
对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与 C 语言的 strstr() 以及 Java 的 indexOf() 定义相符。
示例 1:
输入:haystack = "hello", needle = "ll"
输出:2
示例 2:
输入:haystack = "aaaaa", needle = "bba"
输出:-1
示例 3:
输入:haystack = "", needle = ""
输出:0
提示:
0 <= haystack.length, needle.length <= 5 * 104
haystack 和 needle 仅由小写英文字符组成
通过次数354,811,提交次数880,279
class Solution {
public:
void getNext(int* next, const string& s) {
int j = -1;
next[0] = j;
for(int i = 1; i < s.size(); i++) { // 注意i从1开始
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
j = next[j]; // 向前回溯
}
if (s[i] == s[j + 1]) { // 找到相同的前后缀
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
}
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = -1; // // 因为next数组里记录的起始位置为-1
for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始
while(j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配
j = next[j]; // j 寻找之前匹配的位置
}
if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动
j++; // i的增加在for循环里
}
if (j == (needle.size() - 1) ) { // 文本串s里出现了模式串t
return (i - needle.size() + 1);
}
}
return -1;
}
};
树
树的存储结构
三种不同的表示法:双亲表示法、孩子表示法(改进:双亲孩子表示法)、孩子兄弟表示法。
二叉树
二叉树的特点
特殊二叉树
1.同样结点数的二叉树,完全二叉树的深度最小。
注意:完全二叉树包含满二叉树。
2.完全二叉树的叶子结点只会出现在最下面的两层。
二叉树的性质
性质1
在 二 叉 树 的 第 i 层 上 至 多 有 2 i − 1 个 结 点 ( i ≥ 1 ) . 在二叉树的第i层上至多有2^{i-1}个结点(i\geq1). 在二叉树的第i层上至多有2i−1个结点(i≥1).
性质2
深 度 为 k 的 二 叉 树 至 多 有 2 k − 1 个 结 点 ( k ≥ 1 ) . 深度为k的二叉树至多有2^{k}-1个结点(k\geq1). 深度为k的二叉树至多有2k−1个结点(k≥1).
性质2的推论
1.
深
度
为
k
的
满
二
叉
树
的
结
点
数
n
一
定
是
2
k
−
1.
1.深度为k的满二叉树的结点数n一定是2^k-1.
1.深度为k的满二叉树的结点数n一定是2k−1.
2.
由
n
=
2
k
−
1
倒
推
得
到
结
点
数
为
n
的
满
二
叉
树
的
深
度
为
k
=
l
o
g
2
(
n
+
1
)
.
2.由n=2^k-1倒推得到结点数为n的满二叉树的深度为k=log_2(n+1).
2.由n=2k−1倒推得到结点数为n的满二叉树的深度为k=log2(n+1).
性质3
对 任 何 一 棵 二 叉 树 T , 如 果 其 终 端 结 点 数 为 n 0 , 度 为 2 的 结 点 数 为 n 2 , 则 n 0 = n 2 + 1. 对任何一棵二叉树T,如果其终端结点数为n_0,度为2的结点数为n_2,则n_0=n_2+1. 对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1.
性质4
具 有 n 个 结 点 的 完 全 二 叉 树 的 深 度 为 [ l o g 2 n ] + 1 ( [ x ] 表 示 不 大 于 x 的 最 大 整 数 ) . 具有n个结点的完全二叉树的深度为[log_2n]+1([x]表示不大于x的最大整数). 具有n个结点的完全二叉树的深度为[log2n]+1([x]表示不大于x的最大整数).
性质5
如
果
对
一
棵
有
n
个
结
点
的
完
全
二
叉
树
(
其
深
度
为
[
l
o
g
2
n
]
+
1
)
的
结
点
按
层
序
编
号
(
从
第
1
层
到
底
[
l
o
g
2
n
]
+
1
层
,
每
层
从
左
到
右
)
,
对
任
一
结
点
i
(
1
≤
i
≤
n
)
有
:
如果对一棵有n个结点的完全二叉树(其深度为[log_2n]+1)的结点按层序编号(从第1层到底[log_2n]+1层,每层从左到右),对任一结点i(1 \leq i \leq n)有:
如果对一棵有n个结点的完全二叉树(其深度为[log2n]+1)的结点按层序编号(从第1层到底[log2n]+1层,每层从左到右),对任一结点i(1≤i≤n)有:
1.
如
果
i
=
1
,
则
结
点
i
是
二
叉
树
的
根
,
无
双
亲
;
如
果
i
>
1
,
则
其
双
亲
是
结
点
[
i
/
2
]
.
1.如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点[i/2].
1.如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点[i/2].
2.
如
果
2
i
>
n
,
则
结
点
i
无
左
孩
子
(
结
点
i
为
叶
子
结
点
)
;
否
则
其
左
孩
子
是
结
点
2
i
.
2.如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i.
2.如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i.
3.
如
果
2
i
+
1
>
n
,
则
结
点
i
无
右
孩
子
;
否
则
其
右
孩
子
是
结
点
2
i
+
1.
3.如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1.
3.如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1.
二叉树的存储结构
二叉树顺序存储结构
顺序存储结构一般只用于完全二叉树。
二叉链表
结点结构图:
lchild | data | rchild |
---|
与树的存储结构一样,如果有需要,可以再增加一个指向其双亲的指针域,那样就称之为三叉链表。
遍历二叉树(traversing binary tree)
对于二叉树的遍历来讲,次序是很重要的一件事。
二叉树遍历方法
前序遍历
前序遍历时,根结点首先被遍历。
二叉树的定义是用递归的方式,所以,实现遍历算法也可以采用递归。
void preOrderTraverse(biTree T){
if(T==NULL) return;
cout<<T->data;
preOrderTraverse(T->lchild); // 先序遍历左子树
preOrderTraverse(T->rchild); // 先序遍历右子树
}
中序遍历
中序遍历和前序遍历仅仅存在代码顺序上的差异,相当于把调用左孩子的递归函数提前了。
void inOrderTraverse(biTree T){
if(T==NULL) return;
inOrderTraverse(T->lchild); // 中序遍历左子树
cout<<T->data;
inOrderTraverse(T->rchild); // 中序遍历右子树
}
后序遍历
后序遍历时,根结点最后遍历。
同样地,
void postOrderTraverse(biTree T){
if(T==NULL) return;
postOrderTraverse(T->lchild); // 后序遍历左子树
postOrderTraverse(T->rchild); // 后序遍历右子树
cout<<T->data;
}
层序遍历
推导遍历结果
二叉树遍历的性质
·已知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。
·已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。
需要注意的是,已知前序和后序遍历序列,是不能确定一棵二叉树的。
二叉树的建立
//用前序遍历的方式递归建立二叉树
//AB#D##C##
void createBiTree(biTree *T){
TElemType ch;
scanf("%c",&ch);
if(ch=='#') *T=NULL;
else{
*T=(biTree)malloc(sizeof(biTNode));
if(!*T) exit(OVERFLOW); // 建立新结点失败
(*T)->data=ch; // 生成根结点
createBiTree(&(*T)->lchild); // 构造左子树
createBiTree(&(*T)->rchild); // 构造右子树
}
}
也可以用中序或者后序遍历的方式实现二叉树的建立,只不过代码里生成结点和构造左右子树的代码顺序交换一下。
另外,输入的字符也要做相应的更改。比如中序遍历字符串就应该为#B#D#A#C#,而后序字符串应该为###DB##CA。
线索二叉树(Threaded Binary Tree)
线索二叉树原理
对于一个有n个结点的二叉链表,每个结点有指向左右孩子的两个指针域,所以一共有2n个指针域。而n个结点的二叉树一共有n-1条分支线数,也就是说,存在2n-(n-1)=n+1个空指针域,浪费内存资源。
线索二叉树结构实现
由于前驱和后继的信息只有在遍历该二叉树时才能得到,所以线索化的过程就是在遍历的过程中修改空指针的过程。
树、森林与二叉树的转换
树转换为二叉树
森林转换为二叉树
二叉树转换为树
二叉树转换为树是树转换为二叉树的逆过程。
二叉树转换为森林
判断一棵二叉树能够转换成一棵树还是森林,只需要看这棵二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树。
树与森林的遍历
哈夫曼树(Huffman Tree)及其应用
哈夫曼编码
无损编码、无错解码。
图
图的存储结构
邻接矩阵
无向图的边数组是一个对称矩阵。
/* 这里有向图的邻接点判定方法让我感到迷惑(图中
V
3
V_3
V3的邻接点为0)?😂 */
邻接表
十字链表
对于有向图而言,邻接表是有缺陷的。关心了出度问题,想了解入度就必须要遍历整个图才能知道,反之,逆邻接表解决了入度却不了解出度的情况。
有向图的一种存储方法,把邻接表与逆邻接表结合起来的方法:十字链表(Orthogonal List),解决了这个问题。
不得不吐槽一句,这本书中的小错误真不少。
邻接多重表
边集数组
图的遍历
深度优先遍历(DFS)
深度优先遍历(Depth_First_Search),也有称为深度优先搜索,简称为DFS。
对比邻接矩阵和邻接表的深度优先遍历算法,对于n个顶点e条边的图来说。由于邻接矩阵是二维数组,要查找每个顶点的邻接点需要访问矩阵中的所有元素,因此需要
O
(
n
2
)
O(n^2)
O(n2)的时间。而邻接表做存储结构时,找邻接点所需的时间取决于顶点和边的数量,所以是O(n+e)。显然,对于点多边少的稀疏图来说,邻接表结构使得算法在时间效率上大大提高。
广度优先遍历(BFS)
广度优先遍历(Breadth_First_Search),又称为广度优先搜索,简称BFS。
对比图的深度优先遍历与广度优先遍历算法,会发现,他们在时间复杂度上是一样的,不同之处仅在于对顶点访问的顺序不同。二者在全图遍历上是没有优劣之分的,视不同的情况选择不同的算法。
深度优先更适合目标比较明确,以找到目标为主要目的的情况,而广度优先更适合在不断扩大遍历范围时找到相对最优解的情况。
最小生成树
我们把构造连通网的最小代价生成树称为最小生成树(Minimum Cost Spanning Tree)。
普里姆(Prim)算法
克鲁斯卡尔(Kruskal)算法
对比两个算法,克鲁斯卡尔算法主要是针对边来展开,边数少时效率会非常高,所以对于稀疏图有很大的优势;而普里姆算法对于稠密图,即边数非常多的情况会更好一些。
另外,Prim算法像是走一步看一步的思维方式,逐步生成最小生成树。而Kruskal算法则更有全局意识,直接从图中最短权值的边入手,寻找最终答案。
最短路径
迪杰斯特拉(Dijkstra)算法
点击跳转>>萌新都能看懂的Dijkstra算法
弗洛伊德(Floyd)算法
点击跳转>>萌新都能看懂的Floyd算法
拓扑排序
AOV(Activity On Vertex network)网中的弧表示活动之间存在的某种制约关系,另外AOV网中不能存在回路,也就是说,让某个活动的开始要以自己完成作为先决条件,禁止套娃!
⚠️注意区分其与有向图的遍历的不同之处,下图:
关键路径
如果要对一个流程图获得最短时间,就必须要分析他们的拓扑关系,并且找到当中最关键的流程,这个流程的时间就是最短时间。
AOE(Activity On Edge network)网。
如果是多条关键路径,则单是提高一条关键路径上的关键活动的速度并不能导致整个工程缩短工期,而必须同时提高在几条关键路径上的活动的速度(主要矛盾和次要矛盾,主要矛盾中的主要部分和次要部分)。
点击跳转>>萌新都能看懂的AOE网扩展阅读
- 《大话数据结构》学习笔记(一)完结