说明:用typora编写md文件时中的图片都是存储在本地的,这样就导致在别人拿到这个md文件时无法看到里面的图片,所以我先用csdn的这个Markdown编辑器进行笔记的记录,然后粘贴复制到typora中,这时候的图片都是存储在服务器上的。。
这样整好像有点麻烦,,不过typora现在的有新版本已经支持将图片整成服务器地址了,感兴趣的可以读读这篇文章:
史上最强markdown编辑器typora终于支持图床功能了!!!
二叉树的遍历有:先序,中序,后序,层序遍历。先序,中序,后序其实就是图的深度优先搜索(DFS);而层序遍历则是图的广度优先搜索(BFS)。
不论是DFS还是BFS,都可以用递归实现。当然,也可以借助堆栈,用循环实现。DFS适合用栈(Stack),BFS使用用队列(Queue),切记切记。递归优雅简洁,循环可以让你通晓细节。平时学习过程可以考虑用两种方式实现。
C++解法:
循环里套递归 , 需要注意的点是考虑好递归结束条件 以及 结束条件在调用递归的上方进行
还有注意的一点是:不想改变某个值时,可以把他当成某个参数传进函数里
满二叉树从右至左、从下往上删除一些节点,剩余的结构就构成完全二叉树(就是添加子树时候必须从左面先添加)
通俗来讲,二叉树在按层序遍历时在遇到第一个NULL指针即作为结尾的二叉树就可以称之为完全二叉树。
完全二叉树是一种非常高效和紧凑的树结构,
堆这种数据结构就是基于完全二叉树实现的,堆的逻辑结构就是完全二叉树,其物理结构常用数组存储,常见的场景就是海量数据求Top N的问题
二叉排序树(二叉搜索树)的问题在于,如果树本身不是均衡的会导致树退化成链表,这个时候所有操作的效率都是O(N),效率大大降低,所以出现了平衡二叉树:
平衡二叉树(Self-balancing binary search tree)又被称为AVL树(有别于AVL算法),且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
平衡二叉树一般都是在二叉搜索树的基础之上添加自动维持平衡的性质,这种树的插入,搜索,删除的综合效率比较高为O(logN),AVL的优势在于查找,但是插入和删除操作会破坏AVL的平衡性,然后需要旋转什么的才能再次平衡,所以这时候需要红黑树了
比如前面介绍的AVL树(严格平衡的二叉树)和红黑树(非严格平衡的二叉树)。
红黑树:
红黑树是一棵二叉搜索树,它在每个节点增加了一个存储位记录节点的颜色,可以是RED,也可以是BLACK;通过任意一条从根到叶子简单路径上颜色的约束,红黑树保证最长路径不超过最短路径的二倍,因而近似平衡。
具体性质如下:
每个节点颜色不是黑色,就是红色
根节点是黑色的
如果一个节点是红色,那么它的两个子节点就是黑色的(没有连续的红节点)
对于每个节点,从该节点到其后代叶节点的简单路径上,均包含相同数目的黑色节点
哈弗曼树:哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
STL自带了一个堆priority_queue,还有一套可以用来把迭代器指定的range当堆操作的通用算法在头里,make_heap、push_heap、pop_heap、sort_heap那几个,如果堆可以满足需求就不要用set/multiset,树结构需要多次堆分配而且不如连续存储cache friendly
若用make_heap(), pop_heap(), push_heap(),需要添加头文件# include 。
用less ()和greater () 需要添加头文件 # include
它底层使用平衡的搜索树——红黑树实现,插入删除操作时仅仅需要指针操作节点即可完成,不涉及到内存移动和拷贝,所以效率比较高。set,顾名思义是“集合”的意思,在set中元素都是唯一的,而且默认情况下会对元素自动进行升序排列,支持集合的交(set_intersection),差(set_difference) 并(set_union),对称差(set_symmetric_difference) 等一些集合上的操作,如果需要集合中的元素允许重复那么可以使用multiset。
map() 会根据提供的函数对指定序列做映射。
第一个参数 function 以参数序列中的每一个元素调用 function 函数,返回包含每次 function 函数返回值的新列表。
第二个参数:iterable – 一个或多个序列
(一个接口,多个方法,接口是指:基类指针指向子类的实例)
总结为:在基类与子类函数名相同的前提下,根据参数是否相同、是否具有vritual关键字,可分为4种情况:
参数相同、有virtual关键字:多态重写;
参数相同、无virtual关键字:隐藏;与重写区分。(重写有两种情况:重写成员函数和重写虚函数,而这里只有一种)
参数不同、有virtual关键字:隐藏;与重载区分。(重载是在类内)
参数不同、无virtual关键字:隐藏;与重载区分。
我们知道,数据结构的物理存储结构只有两种:顺序存储结构和链式存储结构(像栈,队列,树,图等是从逻辑结构去抽象的,映射到内存中,也这两种物理组织形式),而在上面我们提到过,在数组中根据下标查找某个元素,一次定位就可以达到,哈希表利用了这种特性,哈希表的主干就是数组。
之所以选择红黑树,是因为二叉树(比如二叉搜索树)在特定情况下,会形成一条线的结构,这跟链表查询的一样了,造成查询很深的问题,遍历查询会变的非常的慢,
红黑树就是为了查询速度快,解决链表查询深度的问题,我们知道红黑树是属于平衡二叉树,但是为了保持平衡是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,如果链表的长度很短 的话,使用红黑树,反而会更慢。
(1)Sliding window,滑动窗口类型
滑动窗口类型的题目经常是用来执行数组或是链表上某个区间(窗口)上的操作。比如找最长的全为1的子数组长度。滑动窗口一般从第一个元素开始,一直往右边一个一个元素挪动。当然了,根据题目要求,我们可能有固定窗口大小的情况,也有窗口的大小变化的情况。
(2)two points, 双指针类型 (想想刚做过的逆序对,归并排序是不是也属于这一类?)
双指针是这样的模式:两个指针朝着左右方向移动(双指针分为同向双指针和异向双指针),直到他们有一个或是两个都满足某种条件。双指针通常用在排好序的数组或是链表中寻找对子。 比如,你需要去比较数组中每个元素和其他元素的关系时,你就需要用到双指针了。
我们需要双指针的原因是:如果你只用一个指针的话,你得来回跑才能在数组中找到你需要的答案。这一个指针来来回回的过程就很耗时和浪费空间了 — 这是考虑算法的复杂度分析的时候的重要概念。虽然brute force一个指针的解法可能会奏效,但时间复杂度一般会是O(n2)。在很多情况下,双指针能帮助我们找到空间或是时间复杂度更低的解。
(3)Fast & Slow pointers, 快慢指针类型
快慢指针:这种算法的两个指针的在数组上(或是链表上,序列上)的移动速度不一样。还别说,这种方法在解决有环的链表和数组时特别有用。
(4)Merge Intervals,区间合并类型
区间合并模式是一个用来处理有区间重叠的很高效的技术。在设计到区间的很多问题中,通常咱们需要要么判断是否有重叠,要么合并区间,如果他们重叠的话。这个模式是这么起作用的:
给两个区间,一个是a,另外一个是b。别小看就两个区间,他们之间的关系能跑出来6种情况。
(5)Cyclic sort,循环排序
这种模式讲述的是一直很好玩的方法:可以用来处理数组中的数值限定在一定的区间的问题。这种模式一个个遍历数组中的元素,如果当前这个数它不在其应该在的位置的话,咱们就把它和它应该在的那个位置上的数交换一下。你可以尝试将该数放到其正确的位置上,但这复杂度就会是O(n^2)。这样的话,可能就不是最优解了。因此循环排序的优势就体现出来了。
(6)In-place Reversal of a LinkedList,链表翻转
在众多问题中,题目可能需要你去翻转链表中某一段的节点。通常,要求都是你得 原地翻转 ,就是重复使用这些已经建好的节点, 而不使用额外的空间。 这个时候,原地翻转模式就要发挥威力了。
这种模式每次就翻转一个节点。一般需要用到多个变量,一个变量指向头结点(下图中的current),另外一个(previous)则指向咱们刚刚处理完的那个节点。在这种固定步长的方式下,你需要先将当前节点(current)指向前一个节点(previous),再移动到下一个。同时,你需要将previous总是更新到你刚刚新鲜处理完的节点,以保证正确性
(7)Tree Breadth First Search,树上的BFS
这种模式基于宽搜(Breadth First Search (BFS)),适用于需要遍历一颗树。借助于 队列数据结构, 从而能保证树的节点按照他们的层数打印出来。打印完当前层所有元素,才能执行到下一层。所有这种需要遍历树且需要一层一层遍历的问题,都能用这种模式高效解决。
这种树上的BFS模式是通过把根节点加到队列中,然后不断遍历直到队列为空。每一次循环中,我们都会把队头结点拿出来(remove),然后对其进行必要的操作。在删除每个节点的同时,其孩子节点,都会被加到队列中。
识别树上的BFS模式:
(8)Tree Depth First Search,树上的DFS
树形DFS基于深搜(Depth First Search (DFS))技术来实现树的遍历。
可以用 递归(或是显示栈stack数据结构,如果你想用迭代方式的话) 来记录遍历过程中访问过的父节点。
可以用前中后序进行遍历,然后递归调用左右子树
识别树形DFS:
(9) Two Heaps,双堆类型
很多问题中,我们被告知,我们拿到一大把可以分成两队的数字。为了解决这个问题,我们感兴趣的是,怎么把数字分成两半?使得:小的数字都放在一起,大的放在另外一半。双堆模式就能高效解决此类问题。
正如名字所示,该模式用到了两个堆,是不是很难猜?一个最小堆用来找最小元素;一个最大堆,拿到最大元素。这种模式将一半的元素放在最大堆中,这样你可以从这一堆中秒找到最大元素。同理,把剩下一半丢到最小堆中,O(1)时间找到他们中的最小元素。通过这样的方式,这一大堆元素的中位数就可以从两个堆的堆顶拿到数字,从而计算出来。
判断双堆模式的秘诀:
(10)Subsets,子集类型,一般都是使用多重BFS
超级多的编程面试问题都会涉及到排列和组合问题。子集问题模式讲的是用BFS来处理这些问题。
这个模式是这样的:
给一组数字 [1, 5, 3]
如果判断这种子集模式:
(11)Modified Binary Search,改造过的二分
当你需要解决的问题的输入是排好序的数组,链表,或是排好序的矩阵,要求咱们寻找某些特定元素。这个时候的不二选择就是二分搜索。这种模式是一种超级牛的用二分来解决问题的方式。
(12)Top ‘K’ Elements,前K个系列
任何让我们求解最大/最小/最频繁的K个元素的题,都遵循这种模式。
用来记录这种前K类型的最佳数据结构就是堆了(译者注:在C++中,改了个名,叫优先队列(PriorityQueue))。这种模式借助堆来解决很多这种前K个数值的问题。
这个模式是这样的:
根据题目要求,将K个元素插入到最小堆或是最大堆。
遍历剩下的还没访问的元素,如果当前出来到的这个元素比堆顶元素大,那咱们把堆顶元素先删
除,再加当前元素进去。
注意这种模式下,不需要去排序数组,因为堆具有这种良好的局部有序性
(13) K-way merge,多路归并
K路归并能解决那些涉及到多组排好序的数组的问题。
该模式是这样的运行的:
识别K路归并:
(14) 0/1 Knapsack (Dynamic Programming),0/1背包类型
(15) Topological Sort (Graph),拓扑排序类型
拓扑排序模式用来寻找一种线性的顺序,这些元素之间具有依懒性。比如,如果事件B依赖于事件A,那A在拓扑排序顺序中排在B的前面。
这种模式是这样奏效的:
拓扑排序模式识别:
这里说的尾递归是指 :在return时 递归调用函数 并返回
int count[INT16_MAX] = {0};这种定义是不能用变量做长度的,必须用New关键字才能用变量做长度,new出的数组初始化时直接在后面加(),初始化为0
//int count[INT16_MAX];报错 返回值duplication不对
//因为题意已说明 数字是0到n-1之间 所以INT16_MAX可以换为length
//int count[INT16_MAX] = {0}; //必须初始化为0 否则牛客网上通不过 但在本地能通过
int *count;
count = new int[length]();//用New来创建数组,这样可以在程序运行时动态分配内存 ,不然用变量Length指定数组长度是要报错的
//int count[255] = {0};
int c = 0;
斐波那契数列的例子严格来说不算动态规划,以上旨在演示算法设计螺旋上升的过程。当问题中要求求一个最优解或在代码中看到循环和 max、min 等函数时,十有八九,需要动态规划大显身手。
提供一个大佬研究出来的一个思维框架,辅助你思考状态转移方程:
明确「状态」 -> 定义 dp 数组/函数的含义 -> 明确「选择」-> 明确 base case。
其实状态转移方程直接代表着暴力解法。
那么关于dp数组的遍历方式,是正着遍历还是反着遍历 ,如果仔细观察的话可以发现其中的原因的。你只要把住两点就行了:
(1)、遍历的过程中,所需的状态必须是已经计算出来的。
(2)、遍历的终点必须是存储结果的那个位置。
解决一个回溯问题,实际上就是一个决策树的遍历过程(在前序遍历和后序遍历上做做手脚)。你只需要思考 3 个问题:
(1)路径 : 也就是已经做出的选择。
(2)选择列表:也就是你当前可以做的选择。
(3)结束条件:也就是到达决策树底层,无法再做选择的条件。
实现时的代码框架如下:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
其实想想看,回溯算法和动态规划是不是有点像呢?我们在动态规划系列文章中多次强调,动态规划的三个需要明确的点就是「状态」「选择」和「base case」,是不是就对应着走过的「路径」,当前的「选择列表」和「结束条件」?
某种程度上说,动态规划的暴力求解阶段就是回溯算法。只是有的问题具有重叠子问题性质,可以用 dp table 或者备忘录优化,将递归树大幅剪枝,这就变成了动态规划。而今天的两个问题,都没有重叠子问题,也就是回溯算法问题了,复杂度非常高是不可避免的。
其实 DFS 算法就是回溯算法
BFS算法应用场景:
问题的本质就是让你在一幅「图」中找到从起点start到终点target的最近距离,这个例子听起来很枯燥,但是 BFS 算法问题其实都是在干这个事儿。
解题核心框架如下:
// 计算从起点 start 到终点 target 的最近距离
int BFS(Node start, Node target) {
Queue q; // 核心数据结构
Set visited; // 避免走回头路
q.offer(start); // 将起点加入队列
visited.add(start);
int step = 0; // 记录扩散的步数
while (q not empty) {
int sz = q.size();
/* 将当前队列中的所有节点向四周扩散 */
for (int i = 0; i < sz; i++) {
Node cur = q.poll();
/* 划重点:这里判断是否到达终点 */
if (cur is target)
return step;
/* 将 cur 的相邻节点加入队列 */
for (Node x : cur.adj())
if (x not in visited) {
q.offer(x);
visited.add(x);
}
}
/* 划重点:更新步数在这里 */
step++;
}
}
代码框架:
int binarySearch(int[] nums, int target) {
int left = 0, right = ...;
while(...) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
...
} else if (nums[mid] < target) {
left = ...
} else if (nums[mid] > target) {
right = ...
}
}
return ...;
}
搜索数 以及 左右边界 三种写法统一用闭区间表示,则代码如下:
int binary_search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if(nums[mid] == target) {
// 直接返回
return mid;
}
}
// 直接返回
return -1;
}
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 别返回,锁定左侧边界
right = mid - 1;
}
}
// 最后要检查 left 越界的情况
if (left >= nums.length || nums[left] != target)
return -1;
return left;
}
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 别返回,锁定右侧边界
left = mid + 1;
}
}
// 最后要检查 right 越界的情况
if (right < 0 || nums[right] != target)
return -1;
return right;
}
public class KMP {
private int[][] dp;
private String pat;
public KMP(String pat) {
this.pat = pat;
int M = pat.length();
// dp[状态][字符] = 下个状态
dp = new int[M][256];
// base case
dp[0][pat.charAt(0)] = 1;
// 影子状态 X 初始为 0
int X = 0;
// 构建状态转移图(稍改的更紧凑了)
for (int j = 1; j < M; j++) {
for (int c = 0; c < 256; c++)
dp[j][c] = dp[X][c];
dp[j][pat.charAt(j)] = j + 1;
// 更新影子状态
X = dp[X][pat.charAt(j)];
}
}
public int search(String txt) {
int M = pat.length();
int N = txt.length();
// pat 的初始态为 0
int j = 0;
for (int i = 0; i < N; i++) {
// 计算 pat 的下一个状态
j = dp[j][txt.charAt(i)];
// 到达终止态,返回结果
if (j == M) return i - M + 1;
}
// 没到达终止态,匹配失败
return -1;
}
}
贪心算法可以认为是动态规划算法的一个**特例,**相比动态规划,使用贪心算法需要满足更多的条件(贪心选择性质),但是效率比动态规划要高。
比如说一个算法问题使用暴力解法需要指数级时间,如果能使用动态规划消除重叠子问题,就可以降到多项式级别的时间,如果满足贪心选择性质,那么可以进一步降低时间复杂度,达到线性级别的。
什么是贪心选择性质呢,简单说就是**:每一步都做出一个局部最优的选择,最终的结果就是全局最优。**注意哦,这是一种特殊性质,其实只有一小部分问题拥有这个性质。
注:对角线的元素值已经初始化好的情况下
for(int l=2;l<=n;l++)
{
for(int i=0;i<=n-1;i++)
{
int j = l+i-1;
//对dp[i][j]进行操作
}
注:参考剑指offer53题 判断字符串能否转为数字的答案
例如一个函数的定义如下:
bool isNumeric(char* str)
那我调用函数传实参时,这么初始化:
bool result;
//char *c = "-";
char c[] = "-";
result = s.isNumeric(c);
用char[] 数组来初始化时正确的,char* 初始化时会有warning
参考网址:https://blog.csdn.net/niushuai666/article/details/6662911
#include
#include
#include
#include
#include
using namespace std;
int pre[1010]; //里面全是掌门
int unionsearch(int root)
{
int son, tmp;
son = root;
while(root != pre[root]) //寻找掌门ing……
root = pre[root];
while(son != root) //路径压缩
{
tmp = pre[son];
pre[son] = root;
son = tmp;
}
return root; //掌门驾到~
}
int main()
{
int num, road, total, i, start, end, root1, root2;
while(scanf("%d%d", &num, &road) && num)
{
total = num - 1; //共num-1个门派
for(i = 1; i <= num; ++i) //每条路都是掌门
pre[i] = i;
while(road--)
{
scanf("%d%d", &start, &end); //他俩要结拜
root1 = unionsearch(start);
root2 = unionsearch(end);
if(root1 != root2) //掌门不同?踢馆!~
{
pre[root1] = root2;
total--; //门派少一个,敌人(要建的路)就少一个
}
}
printf("%d\n", total);//天下局势:还剩几个门派
}
return 0;
}
下面是join函数的单独实现:上面那个程序已有
void join(int root1, int root2) //虚竹和周芷若做朋友
{
int x, y;
x = unionsearch(root1);//我老大是玄慈
y = unionsearch(root2);//我老大是灭绝
if(x != y)
pre[x] = y; //打一仗,谁赢就当对方老大
}
两者本质上很像
找工作面试时候的优化算法可能也就这两种:单调队列算法和双指针算法
队列的队头和队尾就像是两个指针
**例题:**剑指offer54题:字符流中第一个只出现一次的字符 (有单调性,答案肯定是从前往后的)
getline(cin, s); // 用getline输入带空格的字符串
(1)根据完全二叉树的定义,如果二叉树上某个结点有右孩子无左孩子则一定不是完全二叉树;
(2)否则如果二叉树上某个结点有左孩子而没有右孩子,那么该结点所在的那一层上,该结点右侧的所有结点应该是叶子结点,否则不是完全二叉树。
import java.util.LinkedList;
import java.util.Queue;
/**
* 判断是否为完全二叉树
*/
public class IsCompleteBTree {
public static class Node {
int data;
Node left;
Node right;
public Node(int data) {
this.data = data;
}
}
public static boolean isCompleteBTree(Node root) {
if (root == null) {
return true;
}
Queue queue = new LinkedList<>();
queue.offer(root);
boolean leaf = false;
while (!queue.isEmpty()) {
Node node = queue.poll();
//左空右不空
if (node.left == null && node.right != null) {
return false;
}
//如果开启了叶子结点阶段,结点不能有左右孩子
if (leaf &&
(node.left != null || node.right != null)) {
return false;
}
//将下一层要遍历的加入到队列中
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
else
{
//左右均为空,或左不空右空。该结点同层的右侧结点均为叶子结点,开启叶子结点阶段
leaf = true;
}
}
return true;
}
}
默认比较函数less,大顶堆
priority_queue q;
使用greater比较函数,小顶堆
priority_queue, greater > q;
参考文献:
1、LeetCode按照怎样的顺序来刷题比较好?
2、