遇到在数组字符串中查找某元素第一时间想到哈希表
查找循环,对比两个数组用快慢指针
在字符串中查找子字符串用kmp算法
数组填充类问题,可以先预先给数组扩容到填充后的大小,然后双指针法从后向前进行操作
统计元素出现的频率用map
二叉树中要在每层中找某值,就用队列层序遍历
一看到二叉搜索树就要想到它的中序遍历是一个有序数组
求排列组合问题,子集问题用回溯算法,先写出回溯模板
在数组中取元素满足总和等于某值,如果元素不能重复取,则看01背包能解否,如果元素可以无限取则看完全背包能解否
单独一句话没有区别,但是作为操作数的时候有区别,++i先对i+1再操作,i++先对i操作在+1
a=b++;//a=b;b+1;
c=++b;//b+1;c=b;
对于乘法或加法判断一定要防溢出
乘法判断转化为除法,加法判断转化为减法
例如:二分查找,用x/mx判断防止溢出3)
科学计数法表示
1e-6
基于范围的for循环
sting t;
for(const auto &c:t){
target[c]++;
}
C++标准库是有多个版本三个最为普遍的STL版本:
1)HP STL 其他版本的C++ STL,一般是以HP STL为蓝本实现出来的,HP STL是C++ STL的第一个实现版本
2)P.J.Plauger STL 由P.J.Plauger参照HP STL实现出来的,被Visual C++编译器所采用,不是开源的。
3)SGI STL 由Silicon Graphics Computer Systems公司参照HP STL实现,被Linux的C++编译器GCC所采用,SGI STL是开源软件,源码可读性甚高。
double t = pow(2, 3);
double t = 2<<1; 相当于2^2
9 &按位与
&可以表示逻辑运算按位与,按位与运算符“&”是双目运算符。其功能是参与运算的两数各对应的二进位相与。只有对应的两个二进位都为1时,结果位才为1。参与运算的两个数均以补码出现。例如:3&10可写算式如下: 00000011&00001010 00000010
10 pair
stack> stc;
stc.push(pair(root,root->val));
stc.push(pair(node.first->left,node.second+node.first->left->val));
11 堆的概念
堆是一棵完全二叉树,同时保证父子节点的顺序关系(有序)。 但完全二叉树一定是平衡二叉树,堆的排序是父节点大于子节点,而搜索树是父节点大于左孩子,小于右孩子,所以堆不是平衡二叉搜索树。
12 switch语句
switch (i){
case 1:printf("Monday\n"); break;
case 2:printf("Tuesday\n"); break;
case 3:printf("Wednesday\n"); break;
case 4:printf("Thursday\n"); break;
case 5:printf("Friday\n"); break;
case 6:printf("Saturday\n"); break;
case 7:printf("Sunday\n"); break;
default:printf("Error\n"); break;//每个条件后面一定要加break
}
13 自定义排序函数sort
sort快排的时间复杂度:O(nlog n)
static bool compare(const vector& a, const vector& b){
if(a[0] > b[0]) return true;
if(a[0] == b[0]) return (a[1] < b[1]);
return false;
}
public:
vector> reconstructQueue(vector>& people) {
sort(people.begin(), people.end(),compare);
14 迭代器的数据类型
用该数据结构::iterator即表示当前数据的迭代器
std::list>::iterator it = que.begin();
vector定义
vector vec(2);//括号中的2为数组长度
//注意:如果使用vec[i]赋值的话一定要事先声明vec的长度!!!!!!!!!!!!!!!!
vector.swap(vec);//清空数组 同时释放内存
return {}; //返回空vector
array a={1,2,3,4,5};
vector> result(5, vector(2));//构造(5,2)的二维数组
vector赋值
vector v1;//声明v1
v1.assign(v2.begin(), v2.end());//将v2赋值给v1
vector v2{1,2,3,4};
vector v3(v2.begin(), v2.end());
function(vector(v1.begin(),v1.end()));//
vector长度
vec2[i].size();
sring str1;//string的长度size()表示有几个数据而不是内存有多长
int leng=str1.size();
元素排序:sort()函数,,时间复杂度为n*log2(n)
sort(nums.begin(),nums.end());//升序
sort(name.rbegin(),name.rend());//降序
sort(res.begin(),res.end(),[&](const typename&a,const typename&b)->bool{
return (输入你的排序标准);
});//自定义比较大小函数
//自定义数据数组排序
bool static cmp (const pair<int, int>& a, const pair<int, int>& b) {
return a.second > b.second; // 按照频率从大到小排序
}
vector<pair<int, int>> vec(map.begin(), map.end());
sort(vec.begin(), vec.end(), cmp); // 给频率排个序
二维数组
vector> a(n, vector(n))
二维数组vector的初始化方法
vector> a(r, vector(c));
int row = a.size(); //获取行数
int column = a[0].size(); //获取列数
方法一:定义时,直接初始化
(1)下面定义的是行为r,列为c的二维数组
vector> ans(r, vector(c));
(2)下面定义的是行为r,列为c的二维数组,初始值为0
vector< vector > a(r, vector(c, 0));
vector a(4,2); //将含有4个数据的一维动态数组初始为2
方法二:用resize来提前构建
(下面定义的是行为r,列为c的二维数组,初始值为0–因为resize默认为0)
vector> new_mat(r);//注意这个r是不可缺少的,规定其有多少行
for(int i=0 ;i
清除元素resize()函数、clear()、erase()
vector sum(n,1);
sum.resize(0); //直接将向量重置
注意:resize()函数使用时不会改变向量已有元素的值,只会在扩大向量大小时填充元素。
vector sum(n,0);
sum.resize(n+1,2); //只会将第n+1个元素变为2,其他不变。c
sum.resize(n,1); //不会改变任何值,只是最后一个元素被删去。
vector sum(3,1);
sum.clear(); //这里清空后依旧可以访问到原来的元素,表明这里的删除只是将指针移动到来开始位置,但并没有回收内存。
vector sum(3,1);
//第一种:指定删除某个位置的元素,it为某个位置的迭代器,删除后所有后面的元素前移一个位置。
sum.erase(it);
//第二种,删除某一段元素,区间是左闭右开,即删除的是[it1,it2),不包括it2指向的元素。
sum.erase(it1,it2);
求最大最小值
max_element()及min_element()函数 时间复杂度 O ( n )
accumulate(v.begin(),v. end(), 0)求和
找某个元素
auto mxnu_index = find(nums.begin(), nums.end(), max_num);
翻转数组
reverse(path.begin(), path.end());
插入操作
vector> que;
for (int i = 0; i < people.size(); i++) {
int position = people[i][1];
que.insert(que.begin() + position, people[i]);
}
在C++中二维数组是连续分布的
int array[2][3] = {
{0, 1, 2},
{3, 4, 5}
};
vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组
1) 构造函数用s构造a
string s= "abdhcuwh";
string a(s);
string subStr(s,1,3);//表示从s的第一位开始的后三位字符构造给substr
string str(s.begin(),s.end()) //以区间s[beg, end]内的字符作为字符串s的初值
string s(str, stridx) //将字符串str中始于stridx的部分作为构造函数的初值
string a;
string s("0123456789");
a = s.substr(); //不加任何参数,代表全部复制
a = s.substr(1,4); //获取字符串s中从下标1开始,长度为4的字符串,是"1234"
a = s.substr(5); //只有一个参数代表从下标5开始一直到结束为止,是"56789"
a.assign(s.begin(),s.end());
int capacity()const; //返回当前容量(即string中不必增加内存即可存放的元素个数)
int max_size()const; //返回string对象中可存放的最大字符串的长度
int size()const; //返回当前字符串的大小
int length()const; //返回当前字符串的长度
bool empty()const; //当前字符串是否为空
void resize(int len,char c);//把字符串当前大小置为len,并用字符c填充不足的部分
int sum = accumulate(data.begin(), data.end(), 0);//求和,最后以恶搞参数指定精度
2) string的find()函数
用于找出字母在字符串中的位置。
find(str,position)
//str:是要找的元素
//position:字符串中的某个位置,表示从从这个位置开始的字符串中找指定元素(不填第二个参数,默认从字符串的开头进行查找)
返回值为目标字符的位置(第一个字符位置为0),当没有找到目标字符时返回npos
**3) a.find_last_of(b)**函数
在a中查找与b共有的子字符串,返回a中最后一个共有元素的位置
string s = "a##123dcb";
string t = "ace";
int a =s.find_last_of(t);
4) 空字符串表示
string str1 = “”; //空字符串 str1.length() 等于 0
string str2 = null; //NULL
5) string取元素地址
用begin()+i
reverse(s.begin()+left, s.begin()+right);
6)resize()函数重新定义大小
resize(n) 调整容器的长度大小,使其能容纳n个元素,如果n小于容器的当前的size,则删除多出来的元素,否则,添加采用值初始化的元素。
s.resize(regin_size+count*2);//重新分配大小
resize(n,t)多一个参数t,将所有新添加的元素初始化为
reserve(n)预分配n个元素的存储空间,改变capacity,指容器在必须分配新存储空间之前可以存储的元素总数
7) erase()函数删除元素
时间复杂度O(n)
str.erase(pos,n) //删除从pos开始的n个字符 string.erase(0,1); 删除第一个字符
str.erase(pos) //删除pos处的一个字符(pos是string类型的迭代器)
8) swap函数
swap(s[i],s[j]);
9) 相关函数
swap(s1,s2) —— 交换两个字符串的内容
s.length() 和 s.size( ) —— 都表示返回字符的数量 (unsigned int 类型)
s.substr(pos) —— 返回 从pos 开始以及之后的字符串(从0开始计数)
s.substr(a,pos) ——返回 a 到 pos 之间的字符串(包括a但是不包括pos)[a,pos)
+= —— 表示两个字符串合并在一起
s.c_str() —— 将内容以C_string返回
s.empty() —— 判断字符串是否为空
str.clear()——清空字符串,让字符串变为空
s.reserve() ——保留一定量内存以容纳一定数量的字符
s1.replace(2,3, “haha")——将s1中下标2 开始的3个字符换成“haha”
s1.insert(5,s2)—— 将s2插入s1下标5的位置
s1.insert(2,s2,5,3)——将s2中下标5开始的3个字符插入s1下标2的位置
string.c_str() ——返回传统的const char * 类型字符串,且该字符串以‘\0’结尾。
s.pop_back()——说明:删除源字符串的最后一个字符,有效的减少它的长度
s.push_back()——在末尾插入元素
s.back()——返回末尾元素
//这三个函数可以把string抽像成stack栈的数据结构
10)string转int
istringstream is("12"); //构造输入字符串流,流的内容初始化为“12”的字符串
int i;
is >> i; //从is流中读入一个int整数存入i中
采用标准库中atoi函数。
string s = "12";
int a = atoi(s.c_str());
stol(s.c_str()));//string转long
int转string的方式
1、采用标准库中的to_string函数。
int i = 12;
cout << std::to_string((char) (i+'0');) << endl;
不需要包含任何头文件,应该是在utility中,但无需包含,直接使用,还定义任何其他内置类型转为string的重载函数,很方便。
2、采用sstream中定义的字符串流对象来实现。
ostringstream os; //构造一个输出字符串流,流内容为空
int i = 12;
os << i; //向输出字符串流中输出int整数i的内容
cout << os.str() << endl; //利用字符串流的str函数获取流中的内容
用Integer类中的静态方法 public static String toString(int i)。
int i = 20;
//方法一
String s1 = String.valueOf(i);
//方法二
String s2 = "" + i;
//方法三
Integer j = i;String s3 = j.toString();
11) string取子串
string substr (size_t pos = 0, size_t len = npos) const;
s.substr(j, i-j);//取从第j个位置开始,到第i个位置结束的字串
string(s.begin()+j, s.begin()+i);
在C++中,set 提供以下三种数据结构,其底层实现以及优劣如下表所示:
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
优先使用unordered_set
1)背景
C++ 11 为 STL 标准库增添了 4 种无序(哈希)容器, unordered_set 容器,可直译为“无序 set 容器”,即 unordered_set 容器和 set 容器很像,唯一的区别就在于 set 容器会自行对存储的数据进行排序,而 unordered_set 容器不会。
2)特性
不再以键值对的形式存储数据,而是直接存储数据的值。
容器内部存储的各个元素的值都互不相等,且不能被修改。
不会对内部存储的数据进行排序
3)使用容器的条件
#include
using namespace std;
4.类模板
unordered_set 容器的类模板定义如下:
template < class Key, //容器中存储元素的类型
class Hash = hash, //确定元素存储位置所用的哈希函数
class Pred = equal_to, //判断各个元素是否相等所用的函数
class Alloc = allocator //指定分配器对象的类型
> class unordered_set;
5)创建unordered_set容器
std::unordered_set uset;
由此,就创建好了一个可存储 string 类型值的 unordered_set 容器,该容器底层采用默认的哈希函数 hash 和比较函数 equal_to。
在创建 unordered_set 容器的同时,可以完成初始化操作
std::unordered_set uset{ "http://c.biancheng.net/c/",
"http://c.biancheng.net/java/",
"http://c.biancheng.net/linux/" };
std::unordered_set uset2(uset);
成员函数 | 功能 |
---|---|
begin() | 返回指向容器中第一个元素的正向迭代器。 |
end(); | 返回指向容器中最后一个元素之后位置的正向迭代器。 |
cbegin() | 和 begin() 功能相同,只不过其返回的是 const 类型的正向迭代器。 |
cend() | 和 end() 功能相同,只不过其返回的是 const 类型的正向迭代器。 |
empty() | 若容器为空,则返回 true;否则 false。 |
size() | 返回当前容器中存有元素的个数。 |
max_size() | 返回容器所能容纳元素的最大个数,不同的操作系统,其返回值亦不相同。 |
find(key) | 查找以值为 key 的元素,如果找到,则返回一个指向该元素的正向迭代器;反之,则返回一个指向容器中最后一个元素之后位置的迭代器(如果 end() 方法返回的迭代器)。 |
count(key) | 在容器中查找值为 key 的元素的个数。 |
insert() | 向容器中添加新元素。 |
erase() | 删除指定元素。 |
clear() | 清空容器,即删除容器中存储的所有元素。 |
unordered_set_name.count(element) ;
此函数接受单个参数element ,表示容器中是否存在需要检查的元素。
如果元素存在于容器中,则此函数返回1,否则返回0。
注意:使用insert()或者erase()函数时候括号里面一定是元素而不是索引值
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O(logn) | O(logn) |
std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O(log n) | O(log n) |
std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O(1) | O(1) |
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。
(Unordered_maps)是用于存储键值和映射值组合成的元素的关联容器,并允许基于其键快速检索各个元素
#include < unordered_map >
unordered_map myMap={{ 5, "张大" },
{ 6, "李五" }};//使用{}赋值
myMap[2] = "李四"; //使用[ ]进行单个插入,若已存在键值2,则赋值修改,若无则插入。
myMap.insert(pair(3, "陈二"));//使用insert和pair插入
unordered_map> mp;
mp[key].emplace_back(str);//在key键处插入元素值 mp[key]表示一个vector的string
find()查找
// 找到了,则返回对应的数字
if(tags_map.find(hand[i]) != tags_map.end())
{
++tags_map[hand[i]];
}
syring t="asdgfjhuir";
unordered_map target
for(const auto &c:t){
target[c]++;
}//把字符串转换成哈希表,键为每个字符,值为出现次数
成员函数 | 功能 |
---|---|
push_back() | 插入元素,需要先构造临时对象 |
emplace_back() | 插入元素,可以不构造临时对象 |
first | 定义中的键 |
second | 定义中的值 |
map的遍历不能用int i 遍历,要用迭代器遍历
map map;
map::iterator it=map.begin();
while(it!=map.end())
{
cout<first<<" "<second<
map map;
for(auto i=map.begin();i!=map.end();i++)
cout<first<<" "<second<
map map;
for (auto& [key, value] : map)
{
cout<
list是一种序列式容器。list容器完成的功能实际上和数据结构中的双向链表是极其相似的,list的每个节点有三个域:前驱元素指针域、数据域和后继元素指针域。
对于迭代器,只能通过“++”或“–”操作,不能+n
forward_list是单链表,只能单向迭代。
与其他序列式容器相比,list的最大缺陷就是不支持随机访问,但是插入和移除元素的效率要更好O(1)。
构造函数
list l1; //空list
list l2(10, 1); //包含10个整型1的list
list l3(l2); //用l2构造l3
int arr[] = { 6,2,7,9 };
list l4(arr, arr + 4); //用迭代器区间构造
函数声明 接口说明
push_front(numbers) //在list首元素前插入元素
pop_front() //删除list中第一个元素
push_back(numbers) //在list尾部插入元素
pop_back() //删除list最后一个元素
auto pos = ++l1.begin(); //list中pos的给定只能通过迭代器赋值
l1.insert(pos, 10); //在指定位置插入元素
l1.insert(pos, 3, 0); //在pos为之前插入3个0
l1.erase(pos); //在指定位置删除元素
l2.swap(l3); //交换两个list中元素
l2.clear(); //清空list中有效元素
插入操作
int pose = 5;
list>::iterator iter = que.begin();
while(pose--){
iter++;
}
que.insert(iter,8);
stack的头文件是#include
stack q; //以int型为例
int x;
q.push(x); //将x压入栈顶
q.pop(); //删除栈顶的元素
q.top(); //返回栈顶的元素
q.size(); //返回栈中元素的个数
q.empty(); //检查栈是否为空,若为空返回true,否则返回false
emplace(); //用传入的参数调用构造函数,在栈顶生成对象。
swap(stack & other_stack);//将当前栈中的元素和参数中的元素交换。参数所包含元素的类型必须和当前栈的相同。对于 stack 对象有一个特例化的全局函数 swap() 可以使用。
stack 容器适配器的模板有两个参数。第一个参数是存储对象的类型,第二个参数是底层容器的类型。stack 的底层容器默认是 deque 容器,因此模板类型其实是 stack
std::stack> fruit;
创建堆栈时,不能在初始化列表中用对象来初始化,但是可以用另一个容器来初始化,只要堆栈的底层容器类型和这个容器的类型相同。例如:
std::list values {1.414, 3.14159265, 2.71828};
std::stack> my_stack (values);
stack 模板定义了拷贝构造函数,因而可以复制现有的 stack 容器:
纯文本复制
std::stack>copy_stack {my_stack}
头文件#include< queue>
初始化,注意:不能用vector容器初始化queue
queue (<数据类型,容器类型>)//初始化时必须要有数据类型,容器可省略,默认为deque 类型
queueq1;
常用函数
push() //队尾插入一个元素
pop() //删除队列第一个元素
size() //返回队列中元素个数
empty() //如果队列空则返回true
front() //返回队列中的第一个元素
back() //返回队列中最后一个元素
示例
queue q;
q.push("first");
q.push("second");
cout<
头文件#include
deque c;//创建一个空deque
c.assign(3, string("qwjy"));//复制3个qwjy复制给c
c.push_back("qw");//尾部插入一个元素
c.push_front("qy");//头部插入一个元素
c.push_back(elem) //在末尾添加元素
c.pop_back() //移除最后一个元素,但是不返回它
c.push_front(elem) //在头部插入 elem 的一个拷贝
c.pop_front() //移除第一元素(但不返回)
c.front() //返回第一元素(不检查是否存在第一元素)
c.back() //返回最末元素(不检查是否存在最未元素)
Deque 与 vector 相比,功能上的差异如下:
deque 两端都能快速安插元素和移除元素(vector 只在尾端逞威风)。这些操作可以在常量时间内完成。
访问元素时 deque 内部结构会多一个间接过程,所以元素的访问和迭代器的动作会稍稍慢一些。
迭代器需要在不同区块间跳转,所以必须是个 smart pointer,不能是寻常 pointer。
在内存区块大小有限制的系统中, deque 可以内含更多元素,因为它使用不止一块内存。 deque 的 max_size() 可能更大。
Deque 不支持对容量和内存重新分配时机的控制。特别要注意的是,除了头尾两端,在任何地点安插或删除元素都将导致指向 deque 元素的任何 pointer、reference 和 iterator 失效。不过,deque 的内存重分配优于 vector,因为其内部结构显示, deque 不必在内存重新分配时复制所有元素。
Deque 会释放不再使用的内存区块。Deque 的内存大小是可缩减的,但要不要这么做,以及如何做,由实现决定。
Deque 的以下特性跟 Vector 相同:
在中段安插、移除元素的速度相对较慢,因为所有元素都需移动以腾出或填补空间。
迭代器属于 random-access iterator (随机访问迭代器)。
C++中deque是stack和queue默认的底层实现容器(这个我们之前已经讲过),deque是可以两边扩展的,而且deque里元素并不是严格的连续分布的。
以下情形最好采用 deque:
你需要在两端安插和移除元素。
无须指向(refer to)容器内的元素。
要求“不再使用的元素必须释放”(不过 C++ standard 对此无任何保证)
构造函数
deque< Elem > c //构造函数,产生一个空 deque,没有任何元素
deque< Elem > c(c2) //Copy 构造函数,建立 c2 的同型 deque 并成为 c2 的一份拷贝(所有元素都被复制)
deque< Elem > c= c2 // Copy 构造函数,建立一个新的 deque 作为 c2 的拷贝(每个元素都被复制)
deque< Elem > c(rv) //Move 构造函数,建立一个新的 deque,取 rvalue rv 的内容(始自C++11)
deque< Elem > c = rv // Move 构造函数,建立一个新的 deque,取 rvalue rv 的内容(始自C++11)
deque< Elem > c(n) //利用元素的 default 构造函数生成一个大小为 n 的 deque
deque< Elem > c(n, elem) //建立一个大小为 n 的 deque,每个元素值都是 elem
deque< Elem > c(beg, end) //建立一个 deque,以区间 [beg, end) 作为元素初值
deque< Elem > c(initlist) //建立一个 deque,以初值列 initlist 的元素为初值(始自C++11)
deque< Elem > c = initlist //建立一个 deque,以初值列 initlist 的元素为初值(始自C++11)
c.~deque() //销毁所有元素,释放内存
c.empty() //返回是否容器为空(相当于 size()==0 但也许较快)
c.size() //返回目前的元素个数
c.max_size() //返回元素个数之最大可能量
c.shrink_to_fit() //要求降低容量,以符合元素个数(始自C++11)
c1 == c2 //返回 c1 是否等于 c2(对每个元素调用==)
c1 != c2 //返回 c1 是否不等于 c2(相当于!(c1==c2))
c1 < c2 //返回 c1 是否小于 c2
c1 > c2 //返回 c1 是否大于 c2(相当于c2= c2 //返回 c1 是否大于等于 c2(相当于! (c1
在优先队列中,优先级高的元素先出队列,并非按照先进先出的要求,类似一个堆(heap)。其模板声明带有三个参数,priority_queue
priority_queue(),默认按照从小到大排列。所以top()返回的是最大值而不是最小值!
使用greater<>后,数据从大到小排列,top()返回的就是最小值而不是最大值!
如果使用了第三个参数,那第二个参数不能省,用作保存数据的容器
priority_queue , greater<>> pq;//这是对的
empty( ) //判断一个队列是否为空
pop( ) //删除队顶元素
push( ) //加入一个元素
size( ) //返回优先队列中拥有的元素个数
top( ) //返回优先队列的队顶元素
优先队列的时间复杂度为O(logn),n为队列中元素的个数,其存取都需要时间。
二叉树可以链式存储,也可以顺序存储
struct TreeNode {//链式存储节点定义
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(NULL), right(NULL) {}
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。深度为k有2^k-1个节点的二叉树
完全二叉树,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置
二叉搜索树:有序树,若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉排序树,
对于二叉搜索树,不需要回溯的过程,因为节点的有序性就帮我们确定了搜索的方向。二叉树的遍历每次可以用节点值的大小判断去左子节点或者右子节点
中序遍历下,输出的二叉搜索树节点的数值是有序序列
**平衡二叉搜索树:**又被称为AVL(Adelson-Velsky and Landis)树、红黑树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是log(n)
数组如何转化成 二叉树:如果父节点的数组下标是i,那么它的左孩子下标就是i * 2 + 1,右孩子下标就是 i * 2 + 2。
// 根据数组构造二叉树
TreeNode* construct_binary_tree(const vector& vec) {
vector vecTree (vec.size(), NULL);
TreeNode* root = NULL;
// 把输入数值数组,先转化为二叉树节点数组
for (int i = 0; i < vec.size(); i++) {
TreeNode* node = NULL;
if (vec[i] != -1) node = new TreeNode(vec[i]); // 用 -1 表示null
vecTree[i] = node;
if (i == 0) root = node;
}
// 遍历一遍,根据规则左右孩子赋值就可以了
// 注意这里 结束规则是 i * 2 + 1 < vec.size(),避免空指针
// 为什么结束规则不能是i * 2 + 2 < arr.length呢?
// 如果i * 2 + 2 < arr.length 是结束条件
// 那么i * 2 + 1这个符合条件的节点就被忽略掉了
// 例如[2,7,9,-1,1,9,6,-1,-1,10] 这样的一个二叉树,最后的10就会被忽略掉
// 遍历一遍,根据规则左右孩子赋值就可以了
for (int i = 0; i * 2 + 1 < vec.size(); i++) {
if (vecTree[i] != NULL) {
// 线性存储转连式存储关键逻辑
vecTree[i]->left = vecTree[i * 2 + 1];
if(i * 2 + 2 < vec.size())
vecTree[i]->right = vecTree[i * 2 + 2];
}
}
return root;
}
git clone https://github.com/youngyangyang04/PowerVim.git
cd PowerVim
sh install.sh
二叉树主要有两种遍历方式:
层次遍历(迭代法)
递归法深度优先搜索返回最大深度
int maxDepth(TreeNode* root) {
if(root == nullptr) return 0;
return max(maxDepth(root->left), maxDepth(root->right))+1;
}
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。,注意是叶子节点。
什么是叶子节点,左右孩子都为空的节点才是叶子节点!
最好情况时间复杂度(best case time complexity);
最坏情况时间复杂度(worst case time complexity);
平均情况时间复杂度(average case time complexity);
均摊时间复杂度(amortized time complexity)。
01背包问题,用二维数组实现,怎么初始化,先遍历 物品还是先遍历背包重量呢?
// 初始化 dp
vector> dp(weight.size(), vector(bagweight + 1, 0));
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
遍历顺序物品在外和背包在外都可以其实都可以!,应为状态转移方程dp [i-1] [j]和dp [i - 1] [j - weight[i]] 都在dp [i] [j]的左上角方向(包括正上方向),那么先遍历物品,再遍历背包的过程都是在左上角取值
01背包问题用一维数组实现,怎么初始化,两个for循环的顺序反过来写行不行?为什么?遍历背包的for循环为什么要从大到小遍历
dp数组初始化都为0
两个for循环顺序不能调换,倒序遍历是为了保证物品i只被放入一次!一旦正序遍历了,那么物品0就会被重复加入多次!所以内外层的for循环也不能调换位置。对于二维dp,dp[i] [j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i] [j]并不会被覆盖!
纯完全背包,要求先用二维dp数组实现,然后再用一维dp数组实现,最后在问,两个for循环的先后是否可以颠倒?为什么?
而完全背包的物品是可以添加多次的,所以要从小到大去遍历。在完全背包中,对于一维dp数组来说,两个for循环嵌套顺序是无所谓的!
用纯完全背包求解装满背包的组合数,两个for循环的先后是否可以颠倒?为什么?
dp[j] += dp[j-nums[i]];
不可以颠倒,外层for循环遍历物品,内层遍历背包求出来的是组合数,如果外层遍历背包内层遍历物品,求出来的是排列数
二分法适用题目:
一定是有排序的,也可以用来查找上下边界
时间复杂度:O(logn)
要注意右区间是否是闭合的,如果是闭合的则更新时候右边-1,左边+1;如果不闭合则只需要左边+1,右边就等于中值meddle,总值,查找过的中值不再下一次遍历的里面。循环不变量
时间复杂度为 O(log n)
定义,左、右值
当左值<=右值循环{
定义中值
判断中值与目标值大小,更新左右值
}
//二分法查找不重复数组中是否有目标值
int left = 0;
int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
//(right-left)>>1 使用右移操作就等于/2
if (nums[middle] > target) {
right = middle - 1; // target 在左区间,所以[left, middle - 1]
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,所以[middle + 1, right]
} else { // nums[middle] == target
return middle; // 数组中找到目标值,直接返回下标
}
}
//二分法查找排序数组中目标值左边界
int left = 0,RightBorder=-2;
int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
//(right-left)>>1 使用右移操作就等于/2
if (nums[middle] > target) {
right = middle - 1; // target 在左区间,所以[left, middle - 1]
} else{
left = middle + 1; // target 在右区间,所以[middle + 1, right]
RightBorder=left
}
}
**适合题目:**在数组或字符串中查找元素,修改元素之类,或者排序,双指针法还可以用来删除链表倒数元素,找两个链从末尾开始数相同长度的元素
双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。
**快慢指针法:**定义一个慢指针,一个快指针,其中慢指针用于更新元素,快指针用于遍历所有元素(注意:用快指针参与判断),不改变其他元素顺序
在不需要操作的指针处慢指针++,在要操作处只有快指针++
class Solution{
public:
int removeElement(vector& nums, int val){
int slowIndex=0;
for(int fastIndex=0; fastIndex
**左右指针法:**左右两边指针往中间靠拢,用右边不等于val的元素替换左边等于val的元素
class Solution{
public:
int removeElement(vector& nums, int val){
int leftIndex=0,rightIndex=nums.size()-1;
while(leftIndex<=rightIndex){
//左指针一直++,直到找到左边等于val的位置
while(leftIndex <= rightIndex && nums[leftIndex] != val){
++leftIndex;
}
//右边指针一直保持在最右边不等于val的元素的位置
while(leftIndex <= rightIndex && nums[rightIndex] == val){
--rightIndex;//遇到相等的时候减一
}
if(leftIndex < rightIndex){
nums[leftIndex]=nums[rightIndex];
leftIndex++;
rightIndex--;
}
}
return leftIndex;
}
};
**适合题目:**数组字符串满足条件的字串
时间复杂度:O(n)
所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,,窗口就是满足条件的字串
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4He8rGU7-1670151435284)(https://code-thinking-1253855093.file.myqcloud.com/pics/%E6%95%B0%E7%BB%84%E6%80%BB%E7%BB%93.png)]
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null。链接的入口节点称为链表的头结点也就是head
链表定义
//单链表
struct ListNode{
int val; //节点存储元素
ListNode* next; //下一个元素的地址
ListNode(int x) : val(x),next(Null){} //节点构造函数
};
ListNode* head = new ListNode(5); //通过自定义构造函数就可以初始化节点,否则不行
ListNode* head = new ListNode(); //注意一定要new出来内存
head -> val =5;
删除倒数第n个元素:使用双指针法,将fast指针先移动n次,让后slow与fast一起移动直到fast到达尾部
移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。要将头结点向后移动一位,或者设置虚拟节点为新头节点。
注意:使用虚拟头指针时,dum_head->next= head
注意:使用delete删除不需要的元素
ListNode* tmp = head;//先用临时节点把要删除的节点存住
head = head->next;//操作之后
delete tmp;//删除
while (head != NULL && head->val == val) { // 注意这里不是if
ListNode* tmp = head;
head = head->next;
delete tmp;
}
// 删除非头结点
ListNode* cur = head;
while (cur != NULL && cur->next!= NULL) {
if (cur->next->val == val) {
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
} else {
cur = cur->next;
}
}
完整代码
class MyLinkedList {
public:
// 定义链表节点结构体
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val):val(val), next(nullptr){}//结构体构造函数
};
// 初始化链表
MyLinkedList() {
_dummyHead = new LinkedNode(0); // 这里定义的头结点是一个虚拟头结点,而不是真正的链表头结点
_size = 0;
}
// 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
int get(int index) {
if (index > (_size - 1) || index < 0) {//注意index的判断
return -1;
}
LinkedNode* cur = _dummyHead->next;
while(index--){ // 如果--index 就会陷入死循环
cur = cur->next;
}
return cur->val;
}
// 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
void addAtHead(int val) {
LinkedNode* newNode = new LinkedNode(val);
newNode->next = _dummyHead->next;
_dummyHead->next = newNode;
_size++;
}
// 在链表最后面添加一个节点
void addAtTail(int val) {
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while(cur->next != nullptr){
cur = cur->next;
}
cur->next = newNode;
_size++;
}
// 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果index大于链表的长度,则返回空
// 如果index小于0,则置为0,作为链表的新头节点。
void addAtIndex(int index, int val) {
if (index > _size || index < 0) {
return;
}
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
_size++;
}
// 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
void deleteAtIndex(int index) {
if (index >= _size || index < 0) {
return;
}
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur ->next;
}
LinkedNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
_size--;
}
// 打印链表
void printLinkedList() {
LinkedNode* cur = _dummyHead;
while (cur->next != nullptr) {
cout << cur->next->val << " ";
cur = cur->next;
}
cout << endl;
}
private:
int _size;
LinkedNode* _dummyHead;
}
(1)双指针法,一前一后指针previous current,将后指针先存起来然后让后指针指向前面指针。对链表的操作先画图
双指针法还可以用来删除链表倒数元素
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* temp; // 保存cur的下一个节点
ListNode* cur = head;
ListNode* pre = NULL;
while(cur) {
temp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next
cur->next = pre; // 翻转操作
// 更新pre 和 cur指针
pre = cur;
cur = temp;
}
return pre;
}
};
(2) 递归法 其实就是双指针法变型,只不过把双指针法的一次循环换成了递归函数内的一次操作,然后函数内调用递归函数。
class Solution {
public:
//==========================递归函数================
ListNode* reverse(ListNode* pre,ListNode* cur){
if(cur == NULL) return pre;
ListNode* temp = cur->next;
cur->next = pre;
// 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
// pre = cur;
// cur = temp;
return reverse(cur,temp);
}
ListNode* reverseList(ListNode* head) {
// 和双指针法初始化是一样的逻辑
// ListNode* cur = head;
// ListNode* pre = NULL;
return reverse(NULL, head);
}
};
1)翻转链表
一前一后指针previous current,将后指针先存起来然后让后指针指向前面指针。对链表的操作先画图
2)删除倒数元素
让快指针比慢指针先走n个单位,然后一起走
3)求两链表相交点
除了直接求两链表长度,然后通过移动长链表到与短链表相同处再查找的方法外
使用双指针法,指针找到尾之后从另一个链表头重新移动,两个指针都交换后最终一定移动到相同位置
3)查找链表环与环入口
让快指针走的是慢指针的两倍,如果存在环,则在环中一定相遇,且相遇点到环入口长度一定等于起点到入口长度
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w5D0ELgA-1670151435286)(https://code-thinking-1253855093.file.myqcloud.com/pics/%E9%93%BE%E8%A1%A8%E6%80%BB%E7%BB%93.png)]
通过关键字 key 和一个映射函数 Hash(key) 计算出对应的值 value,然后把键值对映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做哈希函数(散列函数),用于存放记录的数组叫做 哈希表(散列表)。 哈希表的关键思想是使用哈希函数,将键 key 和值 value 映射到对应表的某个区块中。可以将算法思想分为两个部分:
如果 Hash(key1) 不等于 Hash(key2),那么 key1、key2 一定不相等,反之不一定相等
解决哈希冲突的办法
1)开放地址法(找空位)
开放地址法:指的是将哈希表中的「空地址」向处理冲突开放。当哈希表未满时,处理冲突时需要尝试另外的单元,直到找到空的单元为止。H(i) = (Hash(key) + F(i)) \% m,i = 1, 2, 3, ..., n (n ≤ m - 1)
2)链地址法(同一位置可存多个数据)
将具有相同哈希地址的元素(或记录)存储在同一个线性链表中。 假设哈希函数产生的哈希地址区间为 [0, m - 1],哈希表的表长为 m。则可以将哈希表定义为一个有 m 个头节点组成的链表指针数组 T。
用数组存储字符串中字母出现频率
class Solution {
public:
bool isAnagram(string s, string t) {
int acount[26]={0};
for( auto &c : s){
acount[c-'a']++;
}
for( auto &c : t){
acount[c-'a']--;
}
for(int i=0; i<26; i++){
if(acount[i] != 0) return false;
}
return true;
}
};
要求只有小写字母,那么就给我们浓浓的暗示,用数组作为哈希表
使用数组和set来做哈希法的局限:
使用unordered_set
class Solution {
public:
vector intersection(vector& nums1, vector& nums2) {
unordered_set result_set; // 存放结果,之所以用set是为了给结果集去重
unordered_set nums_set(nums1.begin(), nums1.end());
for (int num : nums2) {
// 发现nums2的元素 在nums_set里又出现过
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
return vector(result_set.begin(), result_set.end());
}
};
C++里的一个库函数 reverse可以反转字符串,头文件algorithm,两个参数首地址和尾地址来指定反转的区域,第二个参数是给的反转部分最后一位的下一位
#include
#include
using namespace std;
int main(){
int f[3] = {-4, 1, 2};
reverse(&f[0], &f[2]); //进行了一次反转
vectorf = {1,2,3,4,5};
reverse(f.begin(), f.end());
}
双指针法去除多余空格,反转整个字符串,反转单词,在字符串左移位、右移位中一样可以使用
暴力法直接在申请一个字符串填入字符
先整体反转再局部反转
class Solution {
public:
string reverseWords(string s) {
string result;
int j=s.size()-1;
for(int i = s.size()-1;i>=0 ; i--){
if(s[i]==' ') continue;
j=i;
while(s[i] !=' ') {
i--;
if(i<0)break;
}
string temp(s,i+1,j-i);
result+=temp;
result.push_back(' ');
}
result.erase(result.end()-1);
return result;
}
};
在文本串中匹配找到模式串
KMP的主要思想是**当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。**遇见冲突位置向前回退
使用前缀表找到之前已匹配记录,前缀包含首字母的子串,后缀包含尾字母的子串,前缀表中存放最长相等前后缀数字,找前面匹配的时候直接回退到前缀表中数字指向的下表,next数组
重要:求next前缀表
(1)初始化
(2)前后缀不相同回退
(3)前后缀相同
(4)更新next数组
void getNext(int* next, const string& s) { //计算一个字符串前缀表next
int j = 0;//j表示最长前缀的末尾,i表示后缀末尾,又是遍历用的值
next[0] = 0;
for(int i = 1; i < s.size(); i++) {
while (j > 0 && s[i] != s[j]) { // j要保证大于0,因为下面有取j-1作为数组下标的操作
j = next[j - 1]; // 注意这里,是要找前一位的对应的回退位置了
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
int strStr(string haystack, string needle) {//使用KMP算法比较字符串
if(needle.size() == 0) return 0;
int next[needle.size()];
getNext(next, needle);
for(int i = 0, j = 0; i 0){
j = next[j-1];
}
if(haystack[i] == needle[j]){
j++;
}
if(j == needle.size()){
return (i-j+1);
}
}
return -1;
}
如果一个字符串可由自己的子字符串周期循环构成,则在s+s去除首尾元素一定可以找到s,或者使用KMP方法得到的next数组最后一个数字满足:size() % (size() - n) ==0
当一个字符串由重复子串组成的,最长相等前后缀不包含的子串就是最小重复子串
STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器),STL中栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现
容器适配器是一个封装了序列容器的类模板,它在一般序列容器的基础上提供了一些不同的功能。之所以称作适配器类,是因为它可以通过适配容器现有的接口来提供不同的功能
栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)
常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的低层结构
也可以指定vector为栈的底层实现,初始化语句如下:
std::stack<int, std::vector<int> > third; // 使用vector为底层容器的栈
**可以出一道面试题:**栈里面的元素在内存中是连续分布的么?
这个问题有两个陷阱:
队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构。
也可以指定list 为起底层实现,初始化queue的语句如下:
std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列
所以STL 队列也不被归类为容器,而被归类为container adapter( 容器适配器)
使用栈可以解决前后匹配问题
string可以直接作为栈的数据结构
stack::top() = string::back();
stack::pop() = string::pop_back();
stack::push(c) = string::push_back();
class MyQueue { //单调队列(从大到小)
public:
deque que; // 使用deque来实现单调队列
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
// 同时pop之前判断队列当前是否为空。
void pop(int value) {
if (!que.empty() && value == que.front()) {
que.pop_front();
}
}
// 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
// 这样就保持了队列里的数值是单调从大到小的了。
void push(int value) {
while (!que.empty() && value > que.back()) {
que.pop_back();
}
que.push_back(value);
}
// 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
int front() {
return que.front();
}
};
堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。
优先级队列对外接口只是从队头取元素,从队尾添加元素,优先级队列内部元素是自动依照元素的权值排列
对所有元素排序时间复杂度是nlog(n)
priority_queue()
class mycomparison {
public:
bool operator()(const pair& lhs, const pair& rhs) {
return lhs.second > rhs.second;
}
};
priority_queue, vector>, mycomparison> pri_que;
二叉树递归遍历总结:
递归函数单独写,传入节点无返回
数组传入要引用,递归调用左右点
数组压入当前值,前序遍历中左右
中序遍历左中右,后序遍历左右中
看到二叉树,看到递归,都会想:返回值、参数是什么?终止条件是什么?单层逻辑是什么?
写递归三要素
递归前序遍历时间复杂度:O(n),其中 nn 是二叉树的节点数。每一个节点恰好被遍历一次。空间复杂度:O(n),为递归过程中栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。
class Solution{
public:
//前序遍历
void traversal(TreeNode* cur, vector& vec){//一定要传入数组的引用,否则值存不进去
if(cur == nullptr) return;//终止条件
//单词遍历操作
vec.push_back(cur->val);//压入中间值
traversal(cur->left,vec);//遍历左边叶子节点
traversal(cur->right,vec);//遍历右边叶子节点
}
};
递归中序遍历
void travrsal(TreeNode* cur, vector &vec){
if(cur == nullptr) return;
traversal(cur->left, vec); //左
vec.push_back(cur->val); //中
traversal(cur->right,vec); //右
}
递归后序遍历
void travrsal(TreeNode* cur, vector& vec){
if(cur == nullptr) return;
traversal(cur->left, vec); //左
traversal(cur->right,vec); //右
vec.push_back(cur->val); //中
}
Morris前序遍历
class Solution {
public:
vector preorderTraversal(TreeNode* root) {
if(root == nullptr) return {};
vector result;
TreeNode* cur = root;
TreeNode* MostRight = nullptr;
//=====Morris遍历
while(cur != nullptr){//当前节点不为空则一直遍历
MostRight = cur->left;
//当前节点的左子节点是否为空
if(MostRight != nullptr){
//左子节点不为空,先找最右节点
while(MostRight ->right != nullptr && MostRight -> right!= cur){
MostRight = MostRight->right;
}
if(MostRight->right == nullptr){//最右节点为空则指向cur
result.emplace_back(cur->val);//当前值遍历存入
MostRight ->right = cur;//最右节点right指向cur
cur = cur->left;
continue;//下一个cur遍历
}
if(MostRight ->right == cur){
MostRight->right = nullptr;
cur = cur->right;
}
}else{//左子节点为空则压入当前值,遍历右子节点
result.emplace_back(cur->val);
cur = cur->right;
}
}
return result;
}
};
迭代遍历总结
迭代遍历申请栈存放节点,在当前节点不为空或者栈不为空时一直循环
每次都先左边的所有节点都压入
1)前序遍历栈每次压入的节点的同时压入数组中,然后当前节点指向栈顶点右边节点,弹出顶点,
2)中序遍历每次弹出的栈顶点存入数组中,节点指向栈顶点的右节点,与前序不同的是数组存入的位置不同
3)后序遍历存左节点与其他一样,如果栈顶点节点的右节点部位空,那么当前节点右移然后在此从头循环;如果该节点右子节点为空或者已经遍历过那么存入该节点,且pre=cur, cur=nullptr, 弹出栈顶
前序后序另一种迭代法,先存入顶点,然后循环中每次数组中存入top的值并pop,再栈中压入右节点和左节点,后序先压入左节点,再压右节点最后反转数组
统一迭代法主要是每次操作中间节点,使用栈压入中间节点后在压入一个空指针,遍历栈直到为空,在循环中弹出顶点如果不为空就按照遍历相反顺序压入左右左三个节点,如果为空就取下一个顶点在结果中存入该节点
时间复杂度:O(n)O(n),其中 nn 为二叉树节点的个数。二叉树的遍历中每个节点会被访问一次且只会被访问一次。
空间复杂度:O(n)O(n)。空间复杂度取决于栈深度,而栈深度在二叉树为一条链的情况下会达到 O(n)O(n) 的级别。
**递归函数什么时候需要返回值?**什么时候不需要返回值?这里总结如下三点:
//自定义栈迭代法==================
//一直压入左子树直到为空,压的值就是顺序;每弹出一个节点指向弹出节点的右边
class Solution {
public:
vector preorderTraversal(TreeNode* root) {
vector res;
if (root == nullptr) return res;
stack stk;
TreeNode* node = root;
while (!stk.empty() || node != nullptr) {
while (node != nullptr) {//一直压入左边值
res.emplace_back(node->val);//结果数组在这里存入值
stk.emplace(node);
node = node->left;
}
node = stk.top();
stk.pop();
node = node->right;
}
return res;
}
};
//==================迭代法2================重要!!!!!!!!!!!
//弹出栈顶,栈顶(中间元素)操作再压入右边左边值
stc.emplace(cur);//先压入顶点
while(!stc.empty()){
cur = stc.top();
stc.pop();
result.emplace_back(cur->val);//结果存入中间节点值
if(cur->right) stc.emplace(cur->right);//先压入右边值,弹出时候就后弹
if(cur->left) stc.emplace(cur->left);
}
迭代中序遍历
//自定义栈迭代法==================
class Solution {
public:
vector inorderTraversal(TreeNode* root) {
if(root == nullptr ) return {};
vector result;
stack stac;
TreeNode* cur = root;
while(cur != nullptr || !stac.empty()){
while(cur!=nullptr){
stac.emplace(cur);
cur = cur->left;
}
cur = stac.top();
stac.pop();
result.emplace_back(cur->val);//结果数组在这里存入值
cur = cur->right;
}
return result;
}
};
迭代后序遍历
//自定义栈迭代法==================
class Solution {
public:
vector postorderTraversal(TreeNode *root) {
vector res;
if (root == nullptr) {
return res;
}
stack stk;
TreeNode *prev = nullptr;
while (root != nullptr || !stk.empty()) {
while (root != nullptr) {
stk.emplace(root);
root = root->left;
}
root = stk.top();
if (root->right == nullptr || root->right == prev) {
res.emplace_back(root->val);
prev = root;
root = nullptr;
stk.pop();
} else {
root = root->right;
}
}
return res;
}
};
//==================迭代法2==修改前序遍历的存入顺序==============重要!!!!!!
stc.emplace(cur);//先压入顶点
while(!stc.empty()){
cur = stc.top();
stc.pop();
result.emplace_back(cur->val);//存入中间节点值
if(cur->left) stc.emplace(cur->left);//先压入左边值,弹出时候就后弹
if(cur->right) stc.emplace(cur->right);
}
reverse(result.begin(),result.end()); //反转一下就是后序遍历结果
二叉树统一迭代法
//前序遍历
//把中间值压入栈后紧接着给他压入一个标志位NULL,每次取栈顶元素没取到null则压入右左中间值,如果取到null则操作该节点
class Solution {
public:
vector preorderTraversal(TreeNode* root) {
vector result;
stack st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop();
if (node != NULL) {
//实际遍历是中左右,栈中先进后出。所以是右左中
if (node->right) st.push(node->right); // 右
if (node->left) st.push(node->left); // 左
st.push(node); // 中
st.push(NULL);
} else {
node = st.top();
st.pop();
result.push_back(node->val);
}
}
return result;
}
};
//中序遍历
class Solution {
public:
vector inorderTraversal(TreeNode* root) {
vector result;
stack st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
if (node->right) st.push(node->right); // 添加右节点(空节点不入栈)
st.push(node); // 添加中节点
st.push(NULL);
if (node->left) st.push(node->left); // 添加左节点(空节点不入栈)
} else { // 只有遇到空节点的时候,才将下一个节点放进结果集
st.pop(); // 将空节点弹出
node = st.top(); // 重新取出栈中元素
st.pop();
result.push_back(node->val); // 加入到结果集
}
}
return result;
}
};
//后序遍历
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
if (root != NULL) st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop();
st.push(node); // 中
st.push(NULL);
if (node->right) st.push(node->right); // 右
if (node->left) st.push(node->left); // 左
} else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->val);
}
}
return result;
}
};
使用队列,每一层的访问使用队列当前长度遍历队列中元素,先把顶点压进去。队列的头存入结果数组,每次弹出一个头就存入该节点的左右节点(确保左右节点不为空)
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> que;
if (root != NULL) que.push(root);
vector<vector<int>> result;
while (!que.empty()) {
int size = que.size();//关键一步!!!!!!!!!!!
vector<int> vec;
// 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();//每次取头元素并弹出
vec.push_back(node->val);
if (node->left) que.push(node->left);//每取一个压两个
if (node->right) que.push(node->right);
}
result.push_back(vec);
}
return result;
}
};
先确定题目是否需要层序遍历,然后再在每层size循环里面修改对应代码
1) 递归前序、中序、后序遍历
void traversal(TreeNode* cur, vector<int>& vec){//一定要传入数组的引用,否则值存不进去
if(cur == nullptr) return;//终止条件
//单词遍历操作
vec.push_back(cur->val);//压入中间值
traversal(cur->left,vec);//遍历左边叶子节点
traversal(cur->right,vec);//遍历右边叶子节点
}
2) 递归翻转二叉树
//==递归翻转二叉树========
TreeNode* invertTree(TreeNode* root) {
if(!root) return root; //终止条件
swap(root->left, root->right); //交换当前节点左右子树
invertTree(root->left); //然后翻转左子树
invertTree(root->right); //翻转右子树
return root;
}
//=====中序遍历递归翻转二叉树========
TreeNode* invertTree(TreeNode* root) {
if (root == NULL) return root;
invertTree(root->left); // 左
swap(root->left, root->right); // 中
invertTree(root->left); // 注意 这里依然要遍历左孩子,因为中间节点已经翻转了
return root;
}
3) 递归求最大最小层数
//=======递归求最大深度=======
int maxDepth(TreeNode* root) {
if(root == nullptr) return 0;
return max(maxDepth(root->left), maxDepth(root->right))+1;
}
//=========递归求最小深度=========
int minDepth(TreeNode* root) {
if(!root) return 0;
int minLayer = INT_MAX;
if(!root->left && !root->right) return 1;
if( root->right) minLayer = min(minDepth(root->right), minLayer);
if(root->left) minLayer = min(minDepth(root->left), minLayer);
return minLayer+1;
}
迭代法使用栈或者队列一次压入两个节点,并进行节点比较
递归法递归到两个节点同时空时返回true,其他情况返回false
bool check(TreeNode* r1, TreeNode* r2){
if(!r1 && !r2) return true;
if(!r1 || !r2) return false;
if(r1->val != r2->val) return false;
return(check(r1->left, r2->left) && check(r1->right,r2->right));
}
判断一个树是否为另一个树的子树:深度搜索——每个节点下的树与目标树暴力匹配,或者使用前序
求深度适合用前序遍历,而求高度适合用后序遍历
二分查找+位运算
对于最大层数为 hh 的完全二叉树,节点个数一定在
[ 2 h , 2 h + 1 − 1 ] [2^h,2^{h+1}-1] [2h,2h+1−1]
[][2h,2h+1−1] 的范围内
判断第 k个节点是否存在呢?如果第 k 个节点位于第 h 层,则 k 的二进制表示包含 h+1 位,其中最高位是 1,其余各位从高到低表示从根节点到第 k个节点的路径,0表示移动到左子节点,1 表示移动到右子节点。通过位运算得到第 k 个节点对应的路径,判断该路径对应的节点是否存在,即可判断第 k个节点是否存在。层序遍历的第k个节点的二进制表示,后面位数的数字表示移动方式
回溯算法,每次遍历的时候弹出节点,记录的对应路径也要回退到与该节点相同
class Solution {
public:
vector binaryTreePaths(TreeNode* root) {
stack treeSt;// 保存树的遍历节点
stack pathSt; // 保存遍历路径的节点
vector result; // 保存最终路径集合
if (root == NULL) return result;
treeSt.push(root);
pathSt.push(to_string(root->val));
while (!treeSt.empty()) {
TreeNode* node = treeSt.top(); treeSt.pop(); // 取出节点 中
string path = pathSt.top();pathSt.pop(); // 取出该节点对应的路径
if (node->left == NULL && node->right == NULL) { // 遇到叶子节点
result.push_back(path);
}
if (node->right) { // 右
treeSt.push(node->right);
pathSt.push(path + "->" + to_string(node->right->val));
}
if (node->left) { // 左
treeSt.push(node->left);
pathSt.push(path + "->" + to_string(node->left->val));
}
}
return result;
}
};
回溯和递归是一一对应的,有一个递归,就要有一个回溯,回溯要和递归永远在一起**,和递归遍历一模一样,只不过在每次调用递归的时候就回溯一下
class Solution {
public:
void traversal(TreeNode* root, vector<int> &path, vector<string> & result){
path.push_back(root->val);
//确定递归终止条件 前序遍历处理中节点,这里中节点是叶子节点
if(root && !root->left && !root->right){//叶子节点
//把path变为string压入result里面
string spath;
for(auto n : path){
spath+= to_string(n);
spath+="->";
}
spath.pop_back();
spath.pop_back();//弹出最后多余的“->”
result.push_back(spath);
return;
}
if(root->left){
traversal(root->left,path,result);
//======回溯,回溯,回溯====!!!!!!!!========!!!!!!
path.pop_back();//用vector来回溯,弹出最后面的值,其实和栈是一样的道理
}
if(root->right){
traversal(root->right,path,result);
path.pop_back();
}
}
vector<string> binaryTreePaths(TreeNode* root) {
if( root == nullptr ) return {};
vector<int> path;
vector<string> result;
traversal(root, path, result);
return result;
}
};
//如果传递的path不是引用,则在左右处理的的时候就不用pop了,因为函数传递的是复制的值,递归完了之后path的值就等于递归前的值
//==========迭代法求路径=========。用一个stack装节点,另一个stack>装每次的路径
class Solution {
public:
bool hasPathSum(TreeNode* root, int targetSum) {
if( !root ) return false;
stack node_stc;
stack> nums_stc;
node_stc.push(root);
nums_stc.push({root->val});
while(!node_stc.empty()){
TreeNode* node = node_stc.top(); node_stc.pop();
vector nums = nums_stc.top(); nums_stc.pop();
int sum = accumulate(nums.begin(), nums.end(), 0);
//遇到叶子节点,此时的vector就是一条路径
if(sum == targetSum && !node->left && !node->right) return true;
if(node->right){
node_stc.push(node->right);
vector temp = nums;
temp.push_back(node->right->val);
nums_stc.push(temp);
}
if(node->left){
node_stc.push(node->left);
vector temp = nums;
temp.push_back(node->left->val);
nums_stc.push(temp);
}
}
return false;
}
};
找树最底层最左下角的值也可以理解为前序遍历时第一次出现的最长路径的顶点值
1)检索二叉搜索树
对于二叉搜索树节点的有序性就帮我们确定了搜索的方向。二叉树的遍历每次可以用节点值得大小判断去左几点或者右子节点
//在二叉搜所树中判断一个数是否存在
TreeNode* searchBST(TreeNode* root, int val) {
if (!root) return nullptr;
while(root){
if(root->val == val) return root;
root = val > root->val ? root->right : root ->left;
}
return nullptr;
}
2)验证二叉搜索树:
中序遍历下,输出的二叉搜索树节点的数值是有序序列,
注意二叉搜索树中不能有重复元素。
陷阱:不能单纯的比较左节点小于中间节点,右节点大于中间节点
二叉搜索树也可以为空
//中序遍历,递归法,验证二叉搜索树
long long maxVal = LONG_MIN; // 因为后台测试数据中有int最小值
bool isValidBST(TreeNode* root) {
if (root == NULL) return true;
bool left = isValidBST(root->left);
// 中序遍历,验证遍历的元素是不是从小到大
if (maxVal < root->val) maxVal = root->val;
else return false;
bool right = isValidBST(root->right);
return left && right;
}
记住搜索二叉树的中序遍历是一个递增的有序数组,那么可以通过这个数组查找节点最小差值,遇到在二叉搜索树上求什么最值,求差值之类的,都要思考一下二叉搜索树可是有序的,要利用好这一特点
3)找二叉树的公共节点
二叉搜索树的公共节点一定是首次出现落在区间p q中的那个节点
//普通的二叉树寻找公共节点
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == q || root == p || root == NULL) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if (left != NULL && right != NULL) return root;
if (left == NULL) return right;
return left;
}
};
//二叉搜索树寻找公共节点
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if( !root ) return nullptr;
if((root->val >= p->val && root->val <= q->val) || (root->val <=p->val && root->val >= q->val)) return root;
if(root->val < p->val && root->val < q->val) return lowestCommonAncestor(root ->right, p, q);
if(root->val > p->val && root->val > q->val) return lowestCommonAncestor(root ->left, p, q);
return nullptr;
}
4)二叉搜索树插入值
TreeNode* insertIntoBST(TreeNode* root, int val) {
if( !root ) return new TreeNode(val);
if(root->val > val) root->left = insertIntoBST(root->left, val);
if(root->val < val) root->right =insertIntoBST(root->right,val);
return root;
}
5) 二叉树删除节点
//删除二叉树的节点
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == nullptr) return root;
if (root->val == key) {
if (root->right == nullptr) { // 这里第二次操作目标值:最终删除的作用
return root->left;
}
TreeNode *cur = root->right;
while (cur->left) {
cur = cur->left;
}
swap(root->val, cur->val); // 这里第一次操作目标值:交换目标值其右子树最左面节点。
}
root->left = deleteNode(root->left, key);
root->right = deleteNode(root->right, key);
return root;
}
//================修剪搜索二叉树=========================
TreeNode* trimBST(TreeNode* root, int low, int high) {
if( !root ) return nullptr; //节点为空
if( root->val < low) return trimBST(root->right, low, high);
if( root->val > high) return trimBST(root->left, low, high);
root ->left = trimBST( root->left,low, root->val);
root->right = trimBST( root->right, root->val, high);
return root;
}
回溯法,一般可以解决如下几种问题:(集合的子集问题、排列组合问题)
回溯算法模板框架如下:
void backtracking(参数) {//回溯算法需要的参数不容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数。
if (终止条件) {
// 存放结果; 找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。
return;
}
//for循环横向遍历,递归纵向遍历,回溯不断调整结果集
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果;//调用递归就要回溯
}
}
不同集合之间的元素组合,或者同一集合内不同元素的组合
vector vec;
vector> result
void backTrack(int left, int right, int k){
if(vec.size()==k){ //终止条件
result.push_back(vec); //存放结果
return; //函数返回
}
//for(int i = left; i <= right; i++ ){
for(int i = left; i <= right - (k-vec.size())+1; i++ ){//剪枝优化
vec.push_back(i); //处理节点
backTrack(i+1,right,k);//递归
vec.pop_back(); //回溯
}
return;
}
vector> combine(int n, int k) {
vector vec;
vector> result;
backTrack(1,n,k);
return result;
}
对于组合问题,什么时候需要startIndex呢?来控制for循环的起始位置,
如果是一个集合来求组合的话,就需要startIndex,
如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,
注意:是否去重
集合(数组candidates)有重复元素,但还不能有重复的组合,要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重,强调一下,树层去重的话,需要对数组排序
在一个数组或者字符串中添加切割线,不数组分成多段
分割回文串,复原IP地址,,切割问题就可以使用回溯搜索法把所有可能性搜出来startIndex一定是需要的,因为不能重复分割,记录下一层递归分割的起始位置。
//复原IP地址
vector result;
string path;
bool isIp(const string & temp){
if(temp.size() == 0) return false;
if( atoi(temp.c_str()) > 255) return false;
if( temp[0]=='0' && temp.size() >1) return false;
return true;
}
//index表示处理的数组的起始位置,pointnum表示已经插入点的个数
void backTracking(const string &s, int index, int pointNum){
if( pointNum == 3){
string temp(s.begin()+index , s.end());
if(!isIp(temp)) return; //末尾剩下的不能组成ip则返回
path+=temp;
result.push_back(path);
while(path.back() != '.' && path.size()!=0){
path.pop_back();//返回前path要回溯一下
}
return;
}
int m = index+3 < s.size() ? index+3 : s.size();//每次最多就取三个数
for(int i = index; i < m; ++i){
string temp(s.begin()+index , s.begin()+i+1);
if(!isIp(temp)) continue;
path += temp+".";
pointNum++;
backTracking(s,i+1, pointNum);
//---------回溯--------
path.pop_back();
while(path.back() != '.' && path.size()!=0){
path.pop_back();
}
pointNum--;
}
}
vector restoreIpAddresses(string s) {
if( s.size() < 4 || s.size() > 12) return {};
backTracking(s, 0, 0);
return result;
}
就是组合问题的模板,然后主函数调用的时候在加一个for循环,组合问题每次找确定个数的集合,自子集问题找从0 到size()个个数的集合
//===========方法一,大循环+组合问题模板=============
vector> result;
vector path;
//组合问题模板====
void backTracking(vector &nums, int index,int size ){
if(path.size() == size){
result.push_back(path);
return;
}
for(int j = index; j < nums.size(); ++j){
path.push_back(nums[j]);
backTracking(nums,j+1,size);
path.pop_back();
}
}
vector> subsets(vector& nums) {
if( nums.size() == 0) return {{}};
result.push_back({});
//这里用循环多求几次组合
for(int size = 1; size < nums.size()+1; size++){
backTracking(nums, 0,size);
}
return result;
}
//==================方法二,组合问题结果集中存放所有节点
vector> result;
vector path;
void backtracking(vector& nums, int startIndex) {
result.push_back(path); // 收集子集,要放在终止添加的上面,否则会漏掉自己
if (startIndex >= nums.size()) { // 终止条件可以不加
return;
}
for (int i = startIndex; i < nums.size(); i++) {
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
vector> subsets(vector& nums) {
result.clear();
path.clear();
backtracking(nums, 0);
return result;
}
//==========使用set去重===========
vector> result;
vector path;
void backtracking(vector& nums, int startIndex) {
result.push_back(path);
unordered_set uset;//set去重只负责同一层的元素
for (int i = startIndex; i < nums.size(); i++) {
if (uset.find(nums[i]) != uset.end()) {
continue;
}
uset.insert(nums[i]);//不用pop掉元素,因为在同一层每次都重新定义了一个set
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
对一个数组进行重新排序
在子集问题基础上或者在组合问题基础上,添加一个全局的set容器存放每次添加过的值
vector> result;
vector path;
unordered_set unset;
void backTracking(vector& nums){
if(path.size() == nums.size()){
result.push_back(path);
return;
}
for(int i = 0; i < nums.size(); ++i){
if(unset.find(nums[i]) != unset.end()) continue;
unset.insert(nums[i]);
path.push_back(nums[i]);
backTracking(nums);
unset.erase(nums[i]);
path.pop_back();
}
}
vector> permute(vector& nums) {
backTracking(nums);
return result;
}
求局部最优解,如何通过局部最优,推出整体最优,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。
贪心算法步骤:一般分为如下四步:
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
跳跃数组求解
int jump(vector& nums) {
if(nums.size() == 0) return 0;
int maxjum = nums[0];
int result = 0;
int curmaxjump = 0;//当前可移动到的最大位置
int backmaxjump = 0;//下一步可移动的最大位置
for(int i = 0; i < nums.size(); ++i){
if(curmaxjump >= nums.size()-1) break;
backmaxjump = max(nums[i] + i, backmaxjump);//记录每一个位置可以跳跃到的最大位置
if( i >= curmaxjump){//如果这一个位置前面最大只能跳到这里,则步树+1
result++;
curmaxjump = backmaxjump;
}
}
return result;
}
用贪心的方式求最大子序列和的思路为
局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。从而推出全局最优:选取最大“连续和”
在出现两个维度相互影响的情况时,两边一起考虑一定会顾此失彼,要先确定一个维度,再确定另一个维度。
分发糖果,从左到右遍历,然后从右到左遍历
数组重叠区间相关问题
先排序(注意要自定义排序比较函数),然后讨论相邻两边界的左右区间
static bool compare(const vector& a , const vector& b){
return a[0] < b[0];
}
int eraseOverlapIntervals(vector>& intervals) {
sort(intervals.begin(), intervals.end(), compare);
int remove = 0;
for(int i = 1; i < intervals.size(); ++i){
if(intervals[i][0] < intervals[i-1][1]){
intervals[i][1] = min(intervals[i-1][1], intervals[i][1]);
remove++;
}
}
return remove;
}
有手续费的买卖股票
int maxProfit(vector& prices, int fee) {
int result = 0;
int minm = prices[0];
for(int i = 1; i < prices.size(); ++i){
if( prices[i] < minm) minm = prices[i];
if( prices[i] >= minm && prices[i] - minm <= fee) continue;
if( prices[i] - minm > fee){
result += prices[i] - minm -fee;
minm = prices[i] - fee;//后续如果还有高价的时候把这个卖出的手续费补起来!!!!!!!!!!!!!!!!!!关键步骤
}
}
return result;
}
监控二叉树
后续遍历,叶子节点一定不放摄像头
int result = 0;
int backtraversal(TreeNode* root){
//返回值0 表示此处要有摄像头,1 表示此处无摄像头并且未被覆盖,2表示此节点无摄像头但是被覆盖
if(!root) return 2;//空节点右覆盖,那么叶子节点就不需要覆盖
int left = backtraversal( root->left);//二叉树后续遍历,左右中
int right = backtraversal(root->right);
//中间节点处理逻辑
if( left == 2 && right == 2) return 1;//子节点均被覆盖,该节点无需安装摄像头,所以该节点就没有被覆盖
if( left == 1 || right == 1) {//子节点中有一个没有被覆盖
result++;
return 0;
}
if( left == 0 || right == 0) return 2;//表示此节点无需安装摄像头但是被子节点的摄像头覆盖覆盖
return -1;
}
int minCameraCover(TreeNode* root) {
if( backtraversal(root) == 1) result++;
// backtraversal( root );
return result;
}
动态规划(Dynamic Programming,DP),如果某一问题有很多重叠子问题,使用动态规划是最有效的。动态规划中每一个状态一定是由上一个状态推导出来的
动态规划问题解题步骤
确定dp数组(dp table)以及下标的含义 |
---|
确定递推公式(状态转移公式) |
dp数组如何初始化 |
确定遍历顺序 |
举例推导dp数组 |
写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍
可以直接推导出dp数组的递推公式
int fib(int N) {
if (N <= 1) return N;
vector dp(N + 1);//定义dp数组,一定要明白数组存储的是什么内容
dp[0] = 0;//初始化
dp[1] = 1;
for (int i = 2; i <= N; i++) {
dp[i] = dp[i - 1] + dp[i - 2];//状态转移公式
}
return dp[N];
}
找一共有多少条路径
一个 m x n 网格=,每次只能向下或者向右移动一步,达到网格的右下角下问总共有多少条不同的路径?
//动态规划=================================
int uniquePaths(int m, int n) {
vector> dp( m , vector(n, 1));
if(min(m, n) == 1) return 1;
if(min(m, n)== 2) return max(m, n);
for(int i = 1; i < m; ++i){
for(int j = 1; j < n; ++j){
dp[i][j] = dp[i-1][j] + dp[i][j-1];//每一个空格的走法就是上面一个空格加左边的空格
}
}
return dp[m-1][n-1];
}
数学推导,在m+n-2个总步数中找到m-1个步数的组合
C m + n − 2 m − 1 = ( m + n − 2 ) ! ( m − 1 ) ! C n m = n ! m ! ( n − m ) ! C^{m-1}_{m+n-2} = {(m+n-2)!\over(m-1)!}\\ C^m_n={n!\over m!(n-m)!} Cm+n−2m−1=(m−1)!(m+n−2)!Cnm=m!(n−m)!n!
int uniquePaths(int m, int n) {
long long numerator = 1; // 分子
int denominator = m - 1; // 分母
int count = m - 1;
int t = m + n - 2;
while (count--) {
numerator *= (t--);
while (denominator != 0 && numerator % denominator == 0) {
numerator /= denominator;
denominator--;
}
}
return numerator;
}
1~n 整数组成的二叉搜索树个数
递推公式可能是由前面dp[i]所有元素一起构成,这时候就要用for循环求dp[i],外层for循环是遍历,内层for循环是求dp[i]
int numTrees(int n) {
if(n <= 2) return n;
vector dp(n+1);
dp[0] = 1;
for(int i = 0; i <= n; ++i){//这里的遍历顺序表示从1到n为头节点时候个数
for(int j = 0; j < i; ++j){//内层for循环求dp[i]
dp[i] += dp[j] * dp[i-j-1];
}
}
return dp[n];
}
拆分整数使其乘积最大化
int integerBreak(int n) {
vector dp(n+1);
dp[2] = 1;
for (int i = 3; i <= n ; i++) {
for (int j = 1; j < i - 1; j++) {//内层for循环求dp[i]
dp[i] = max(dp[i],max(j * (i - j), dp[i - j] * j));
}
}
return dp[n];
}
能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - weights[i]] + values[i]);
装满背包有几种方法:dp[j] += dp[j - nums[i]]; dp[0] =1;
装满背包最多能装几个物品:dp[i] = max(dp[i], dp[i - weight[i]] +1); dp[0] = 1;
问装满背包所用物品的最小个数:dp[j] = min(dp[j], dp[j - coins[i]] + 1); dp[0] =0 其余为max
状态转移公式
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);//求装满背包的最大价值
dp[j] += dp[j - coins[i]];//求装满背包的最多方式
二维数组n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
确定dp数组以及下标的含义
对于背包问题,有一种写法, 是使用二维数组,即dp[i] [j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
递归公式: dp[i] [j] = max(dp[i - 1] [j], dp[i - 1] [j - weight[i]] + value[i]);
初始化即:dp[i] [0] = 0 dp[0] [weight[]]后面的值就为第0个物品的价值
vector> dp(weight.size(), vector(weight.size(), 0));
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
先遍历物品,然后遍历背包重量; 也可以先遍历背包再遍历物品
// 先遍历物品,再遍历背包,就是常规思维包物品一个一个往背包里面放
// weight数组的大小 就是物品个数
void test_2_wei_bag_problem1() {
vector weight = {1, 3, 4};
vector value = {15, 20, 30};
int bagweight = 4;
// 二维数组
vector> dp(weight.size(), vector(bagweight + 1, 0));
// 初始化
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
cout << dp[weight.size() - 1][bagweight] << endl;
}
// 先遍历背包,再遍历物品,把从0-j容量的背包塞满
// weight数组的大小 就是物品个数
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
for(int i = 1; i < weight.size(); i++) { // 遍历物品
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
推导dp数组
一维滚动数组dp[i]
确定dp数组的定义:在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。
一维dp数组的递推公式:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
一维dp数组如何初始化:dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。
一维dp数组遍历顺序倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历,那么物品0就会被重复加入多次
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量倒叙遍历!!!!!!!!!
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
一维数组求解背包问题
void test_1_wei_bag_problem() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
// 初始化
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}
int main() {
test_1_wei_bag_problem();
}
等于是求给定背包容量,看能不能装满背包,装之后空间最少剩多少
在数组种求和为原数组和一半的子集问题,转为为01背包问题,其中物品的价值和重量都为num[i] , 背包最大容量为sum/2
if (sum % 2 == 1) return false;
int target = sum / 2;
// 开始 01背包
for(int i = 0; i < nums.size(); i++) {
for(int j = target; j >= nums[i]; j--) { // 每一个元素一定是不可重复放入,所以从大到小遍历
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
// 集合中的元素正好可以凑成总和target
if (dp[target] == target) return true;
return false;
}
粉碎石头和等和子集问题一样,就是把石头分成重量最接近的两堆,是求给定背包容量,尽可能装,最多能装多少
装满容量为x背包,有几种方法。是一个组合问题,dp[j] 数组表示:填满j(包括j)这么大容积的包,有dp[j]种方法,对于num[i] 凑成dp[j] 有 dp[j - nums[i]] 种方法,递推公式
dp[j] += dp[j - nums[i]];//dp[0] 一定要初始化为1!!!!!!!!!!!!
vector dp(bagSize + 1, 0);
dp[0] = 1;
for (int i = 0; i < nums.size(); i++) {
for (int j = bagSize; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[bagSize];//装满容量为bagSize的背包有多少种方法
可以理解为物品的值为物品的重量(weight[i]),物品本身的个数相当于物品的价值(value[i])
dp[i] = max(dp[i], dp[i - weight[i]] +1);
//物品的重量为2维的时候
dp[i][j] = max(dp[i][j], dp[i - weig1[x]][j - weig2[x]] + 1);//x是遍历物品的索引
有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大
解法先把多个物品分成一个个的,用01背包解
void test_multi_pack() {
vector weight = {1, 3, 4};
vector value = {15, 20, 30};
vector nums = {2, 3, 2};
int bagWeight = 10;
for (int i = 0; i < nums.size(); i++) {
while (nums[i] > 1) { // nums[i]保留到1,把其他物品都展开
weight.push_back(weight[i]);
value.push_back(value[i]);
nums[i]--;
}
}
//01背包==========================
vector dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
}
时间复杂度:O(m × n × k),m:物品种类个数,n背包容量,k单类物品数量
也可以直接用01背包,在最内层再加一个for循环遍历物品数量
or(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
// 以上为01背包,然后加一个遍历个数
for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) { // 遍历个数
dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i]);
}
}
}
完全背包的物品是可以添加多次的,所以就是一维数组01背包问题内层从小到大去遍历,
// 先遍历物品,在遍历背包
void test_CompletePack() {
vector weight = {1, 3, 4};
vector value = {15, 20, 30};
int bagWeight = 4;
vector dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}
int main() {
test_CompletePack();
}
使用二维数组实现状态转移方程
dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+val[i]);//注意后面是在本层基础上放入当前物品,01背包是在i-1次放入当前物品
for(int i=1; i<=N; i++){
for(int j=0; j<=V; j++)
{
if(j < weight [i])dp[i][j]=dp[i-1][j];//继承上一个背包
if(j >= weight[i])
{ //完全背包状态转移方程
dp[i][j]=max(dp[i-1][j],dp[i][j-weight[i]]+val[i]);
}
}
}
与01背包问题一类似,只是内层循环的顺序从小到大开始
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
dp[j] += dp[j - coins[i]];//状态转移公式!!!!!!!!!!
}
}
注意这里不能交换内外层for循环顺序,如果交换了,那么求得就是组合问题
for (int j = 0; j <= amount; j++) { // 遍历背包容量
for (int i = 0; i < coins.size(); i++) { // 遍历物品
if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
}
}
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
//二维数组方法
int change(int amount, vector& coins) {
//dp的行表示放的是低级个物品,从1开始算
vector> dp(coins.size()+1, vector(amount+1, 0));
for(int i = 0; i < coins.size()+1; ++i) dp[i][0] = 1;
for(int i = 1; i < coins.size()+1; i++){
for(int j=1; j <= amount; j++){
if(j < coins[i-1]){
dp[i][j] = dp[i-1][j];//放不下
}else{
dp[i][j] = dp[i-1][j] + dp[i][j-coins[i-1]];
}
}
}
return dp[coins.size()][amount];
}
时间复杂度:
O ( target × n ) O(\textit{target} \times n) O(target×n)
其中target是目标值,n 是数组nums 的长度。需要计算长度为target}+1 的数组dp 的每个元素的值,对于每个元素,需要遍历数组 nums 之后计算元素值。
空间复杂度:
O ( target ) O(\textit{target}) O(target)
需要创建长度为 target+1 的数组 dp
递推公式
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
int coinChange(vector& coins, int amount) {
vector dp(amount + 1, INT_MAX);//初始化
dp[0] = 0;
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包
if (dp[j - coins[i]] != INT_MAX) { // 如果dp[j - coins[i]]是初始值则跳过
dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
}
}
}
if (dp[amount] == INT_MAX) return -1;
return dp[amount];
}
时时间复杂度:O(n *s ),其中 n 为给定数组的大小,状态转移方程的时间复杂度为s
空间复杂度:O(s)。我们需要 O(s) 的空间保存状态,dp数组大小
单词拆分
bool wordBreak(string s, vector& wordDict) {
vector dp(s.size()+1, false);//dp[i]表示前i个字符串能否拆成单词
dp[0] = true;
unordered_set setWord(wordDict.begin(), wordDict.end());
for(int i = 1; i < s.size()+1; ++i){
for(int j = 0; j <= i; ++j){
string temp(s.begin()+ j, s.begin()+i);
if(dp[j] == true && setWord.find(temp) != setWord.end())
dp[i] = true;
}
}
return dp[s.size()];
}
};
二叉树上进行动态规划,二叉树递归三部 + 动态规划五部曲
偷二叉树的节点
1.确定递归函数的参数和返回值
递归返回值是一个长度为2的数组,表示节点偷与不偷得到的最多的钱,返回数组就是dp数组
vector<int> robTree(TreeNode* cur) {
在递归的过程中,系统栈会保存每一层递归的参数。
2.确定终止条件
在遍历的过程中,如果遇到空节点的话,无论偷还是不偷都是0,这也相当于dp数组的初始化
if (cur == NULL) return vector{0, 0};
使用后序遍历,因为通过递归函数的返回值来做下一步计算。通过递归左节点,得到左节点偷与不偷的金钱。通过递归右节点,得到右节点偷与不偷的金钱。
// 下标0:不偷,下标1:偷
vector<int> left = robTree(cur->left); // 左
vector<int> right = robTree(cur->right); // 右
4.确定单层递归的逻辑
如果是偷当前节点,那么左右孩子就不能偷,如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}
定义二维数组dp[i] [2]
dp数组(dp table)以及下标的含义:dp[i] [0] 表示第i天持有股票所得最多现金 ;dp[i] [1] 表示第i天不持有股票所得最多现金
dp数组递推公式
**dp[i] [0]**应该选:第i-1天就持有股票,就是昨天持有股票的所得现金 即:dp[i - 1] [0];
第i天买入股票,所得现金就是买入今天的股票后所得现金即:-prices[i] ,取现金最大,所以dp[i] [0] = max(dp[i - 1][0], -prices[i]);
**dp[i] [1]**应该选:第i-1天就不持有股票,所得现金就是昨天不持有股票的所得现金即:dp[i - 1] [1];第i天卖出股票,所得现今天价格卖出股票即prices[i] + dp[i - 1][0],取现金最大的,所以dp[i] [0] = max(dp[i - 1][0], -prices[i]);
初始化
那么dp[0][0]表示第0天持有股票,此时就一定是买入股票了,所以dp[0] [0] -= prices[0];
dp[0] [1]表示第0天不持有股票,不持有股票那么现金就是0,所以dp[0] [1] = 0;
1) 全过程只能买一次
int maxProfit(vector& prices) {
int len = prices.size();
if (len == 0) return 0;
vector> dp(len, vector(2));
dp[0][0] -= prices[0];
dp[0][1] = 0;
for (int i = 1; i < len; i++) {
dp[i][0] = max(dp[i - 1][0], -prices[i]);//全程只能买一次,那么本次买入一定是第一次买入
dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
}
return dp[len - 1][1];
}
2) 全程只能买两次
//dp[i][0]表示第i天无操作的最大现金
//dp[i][1]表示第i天买入第一支股票最大现金
//dp[i][2]表示第i天卖出第一支股票最大现金
//dp[i][3]表示第i天买入第二支股票最大现金
//dp[i][4]表示第i天卖出第二支股票最大现金
vector> dp(prices.size(), vector5,0));
dp[0][1] = -prices[0];
dp[0][3] = -prices[0];
for(int i = 1; i < prices.size(); ++i){
dp[0] = dp[0];
dp[1] = max(dp[1], dp[0] - prices[i]);//不买与买
dp[2] = max(dp[2], dp[1] + prices[i]);//不卖与卖
dp[3] = max(dp[3], dp[2] - prices[i]);//不买与买
dp[4] = max(dp[4], dp[3] + prices[i]);//不卖与卖
//cout<
3) 全程可以买k次
int maxProfit(int k, vector& prices) {
vector dp(2 * k + 1, 0);//一维数组版本
for(int i = 1; i < 2 * k; i = i + 2) dp[i] = -prices[0];
for(int i = 1; i < prices.size(); ++i){
for(int j = 1; j < 2 * k + 1; ++j){
if(j % 2 != 0)dp[j] = max(dp[j], dp[j-1] - prices[i]);//买入
if(j % 2 == 0)dp[j] = max(dp[j], dp[j-1] + prices[i]);//卖出
}
}
return dp[2 * k];
}
4) 全程可以买无数次
vector> dp(prices.size(), vector(2,0));
for (int i = 1; i < len; i++) {
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); // 全程可以一直买入卖出,那么本次买入的现金一定是上一次卖出后的钱再去买股票
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
}
5) 含有冷冻期
不能在卖出之后第二天买入
for(int i = 2; i < prices.size(); ++i){
//持有股票只能在前两天卖出股票后买入
dp[i][0] = max(dp[i-1][0], dp[i-2][1] - prices[i]);
dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]);
}
6) 含有手续费
int maxProfit(vector& prices, int fee) {
if(prices.size() == 1) return 0;
vector> dp(prices.size(), vector(2, 0));
dp[0][0] = -prices[0];
for(int i = 1; i < prices.size(); ++i){
dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]);
dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i] - fee);
}
return dp[prices.size()-1][1];
}
最长上升子序列 可以不连续
dp[i]表示i之前包括i的以nums[i]结尾最长上升子序列的长度
//dp[i]表示i之前包括i的以nums[i]结尾最长上升子序列的长度
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
}
if (dp[i] > result) result = dp[i]; // 取长的子序列
}
最长连续递增子序列
不连续递增子序列的跟前0-i 个状态有关,连续递增的子序列只跟前一个状态有关
for (int i = 0; i < nums.size() - 1; i++) {
if (nums[i + 1] > nums[i]) { // 连续记录
dp[i + 1] = dp[i] + 1; // 递推公式
}
}
最长重复子数组,连续的子数组
dp[i] [j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i] [j]。 (特别注意: “以下标i - 1为结尾的A” 标明一定是 以A[i-1]为结尾的字符串 )
//二维数组
vector> dp (A.size() + 1, vector(B.size() + 1, 0));
int result = 0;
for (int i = 1; i <= A.size(); i++) {
for (int j = 1; j <= B.size(); j++) {
if (A[i - 1] == B[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
if (dp[i][j] > result) result = dp[i][j];
}
}
return result;
//滚动数组
vector dp(vector(B.size() + 1, 0));
int result = 0;
for (int i = 1; i <= A.size(); i++) {
for (int j = B.size(); j > 0; j--) {
if (A[i - 1] == B[j - 1]) {
dp[j] = dp[j - 1] + 1;
} else dp[j] = 0; // 注意这里不相等的时候要有赋0的操作
if (dp[j] > result) result = dp[j];
}
}
return result;
最长公共子序列, 可以不连续
vector> dp(text1.size()+1, vector(text2.size()+1, 0));
for(int i = 1; i < text1.size()+1; ++i){
for(int j = 1; j < text2.size()+1; ++j){
if( text1[i-1] == text2[j-1]){
dp[i][j] = dp[i-1][j-1] + 1;
}
else dp[i][j] = max(dp[i][j - 1], dp[i-1][j]);//与最长连续共子序列的区别,不相等的时候最长连续公共子序列为0,不连续就为上一次的值
}
}
return dp[text1.size()][text2.size()];