【声明:文章大部分来自牛客网的题解(小部分是自己的解法)】
文章目录
斐波那契数列
斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家莱昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……
- 使用递归
class Solution {
public:
int Fibonacci(int n) {
if(0 == n || 1 == n)
return n;
return Fibonacci(n-1)+Fibonacci(n-2);
}
};
优点,代码简单好写,缺点:慢,会超时
时间复杂度:O(2^n)
空间复杂度:递归栈的空间
- 使用记忆法
class Solution {
public:
int Fib(int n, vector<int>& dp) {
if (n==0 || n==1) return n;
if (dp[n] != -1) return dp[n];
return dp[n] = Fib(n-1,dp) + Fib(n-2,dp);
}
int Fibonacci(int n) {
vector<int> dp(45, -1); // 因为答案都是>=0 的, 所以初始为-1,表示没计算过
return Fib(n, dp);
}
};
时间复杂度:O(n), 没有重复的计算
空间复杂度:O(n)和递归栈的空间
- 动态规划
虽然方法二可以解决此题了,但是如果想让空间继续优化,那就用动态规划,优化掉递归栈空间。
方法二是从上往下递归的然后再从下往上回溯的,最后回溯的时候来合并子树从而求得答案。
那么动态规划不同的是,不用递归的过程,直接从子树求得答案。过程是从下往上。
int Fibonacci(int n) {
vector<int> dp(n+1, 0);
dp[1] = 1;
for (int i=2; i<=n; ++i) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
时间复杂度:O(n)
空间复杂度:O(n)
###继续优化
发现计算f[5]的时候只用到了f[4]和f[3], 没有用到f[2]…f[0],所以保存f[2]…f[0]是浪费了空间。
只需要用3个变量即可。
int Fibonacci(int n) {
if (n == 0 || n == 1) return n;
int a = 0, b = 1, c;
for (int i=2; i<=n; ++i) {
c = a + b;
a = b;
b = c;
}
return c;
}
时间复杂度:O(n)
空间复杂度:O(1)
跳台阶
题目描述:
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
这是一道经典的递推题目,你可以想如果青蛙当前在第n级台阶上,那它上一步是在哪里呢?
显然,由于它可以跳1级台阶或者2级台阶,所以它上一步必定在第n-1,或者第n-2级台阶,也就是说它跳上n级台阶的跳法数是跳上n-1和跳上n-2级台阶的跳法数之和。
设跳上 n 级台阶有 f(n) 种跳法,则它跳上n级的台阶有 f(n-1)+f(n-2) 种跳法。
然后,我们又思考初始(n=1,n=2)的情况,跳上1级台阶只有1种跳法,跳上2级台阶有2种跳法,最终我们得到如下的递推式:
这个递推式和 比较相似,这里就简单写常用的两种实现方式,详情可以参考这篇博客解法:斐波那契数列(四种解法)。
方法一:
面试别写型递推版实现,时间复杂度O(2^n) 。
public class Solution {
public int JumpFloor(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
return JumpFloor(n - 1) + JumpFloor(n - 2);
}
}
方法二:
面试推荐型,自底向上型循环求解,时间复杂度为O(n) 。
public class Solution {
public int JumpFloor(int target) {
int a = 1, b = 1;
for (int i = 1; i < target; i++) {
a = a + b;
b = a - b;
}
return a;
}
}
变态跳台阶
题目描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
由于它可以跳1级台阶或者2级台阶…,或者n级台阶,所以它上一步必定在第n-1,或者第n-2级台阶,或第n-3级台阶,…,或第0级台阶。
f(0) = 1 : 表示从地上(0级台阶)直接跳到n级台阶
1级台阶 —> f(1) = f(0) = 1
2级台阶 —> f(2) = f(1)+f(0) = 2
3级台阶 —> f(3) = f(2)+f(1)+f(0) = 4
4级台阶 —> f(4) = f(3) + f(2)+f(1)+f(0) = 8
…
n-1级台阶 —> f(n-1) = f(n-2)+…+f(2)+f(1)+f(0)
n级台阶 —> f(n) = f(n-1)+f(n-2)+…+f(2)+f(1)+f(0)
f
(
n
)
=
{
1
n
=
0
1
n
=
1
2
n
=
2
f
(
n
−
1
)
+
f
(
n
−
2
)
+
.
.
.
+
f
(
2
)
+
f
(
1
)
+
f
(
0
)
o
t
h
e
r
f(n)=\begin{cases} 1 & n=0 \\ 1 & n=1 \\ 2 & n=2 \\ f(n-1)+f(n-2)+...+f(2)+f(1)+f(0) & other \end{cases}
f(n)=⎩⎪⎪⎪⎨⎪⎪⎪⎧112f(n−1)+f(n−2)+...+f(2)+f(1)+f(0)n=0n=1n=2other
进一步推导,
f
(
n
)
=
2
∗
f
(
n
−
1
)
,
n
>
0
f(n) = 2 * f(n-1) ,n>0
f(n)=2∗f(n−1),n>0
再进一步推导,
f
(
n
)
=
2
n
−
1
,
n
>
0
f(n) = 2^{n-1} ,n>0
f(n)=2n−1,n>0
//使用记忆法
class Solution {
public:
int Fib(vector<int>& dp, int n){
if(0 == n) return 1;
if(1 == n) return 1;
if(dp[n] != -1) return dp[n];
// 求和
int sum = 0;
for(int i=0; i<n; ++i){
sum += Fib(dp, i);
}
dp[n] = sum;
return dp[n];
}
int jumpFloorII(int number) {
vector<int> res(number+1,-1);// 分配 0 ~ n 个位置
return Fib(res, number);
}
};
或者
class Solution {
vector<int> m_dp;//记忆 f(0),f(1),f(2),...
public:
int Fib(int n){
if(0 == n) return 1;
if(1 == n) return 1;
if(m_dp[n] != -1) return m_dp[n];
int sum = 0;
for(int i=0; i<n; ++i){
sum += Fib(i);
}
m_dp[n] = sum;
return m_dp[n];
}
int jumpFloorII(int number) {
m_dp.assign(number+1,-1);// 分配 0 ~ n 个位置
return Fib(number);
}
};
–》推荐的
int jumpFloorII(int n) {
vector<int> dp(n+1, 0);
dp[0] = 1;
dp[1] = 1;
for (int i=2; i<=n; ++i) {
int sum=0;
for(int j=0;j<i;++j){
sum += dp[j];
}
dp[i] = sum;
}
return dp[n];
}
或者
class Solution {
public:
int jumpFloorII(int n) {
if(0==n || 1==n)
return 1;
int a = 1;
int tmp;
for(int i=0; i<n-1; ++i){
tmp = a << 1;// 口诀:左移乘2,右移除2
a = tmp;
}
// 即 2^(n-1) 等价 pow(2, n-1)
return tmp;
}
};
///
int jumpFloorII(int n) {
if(0==n || 1==n)
return 1;
return static_cast<int>(pow(2, n-1));
}
矩形覆盖
题目描述
我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。
请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?
比如n=3时,2*3的矩形块有3种覆盖方法:
涂掉最后一级矩形的时候,是用什么方式完成的?
n = 1 的时候
只能横着覆盖,一种
n = 2 的时候
可以横着和竖着覆盖,两种
n = 3 的时候
第三级横着覆盖,用了一级,剩下 n = 2,有两种覆盖方法
第三季竖着覆盖,用了两级,剩下 n = 1,有一种覆盖方法
总共有 3 种
n = 4 的时候
第 4 级横着覆盖,用了一级,剩下 n = 3,有三种覆盖方法
第 4 级竖着覆盖,用了两级,剩下 n = 2,有两种覆盖方法
总共有 5 种方法
n = n 的时候
第 n 级横着覆盖,用了一级,剩下 n = n - 1,所以关注第 n - 1 种有几种覆盖方法
第 n 级竖着覆盖,用了两级,剩下 n = n - 2,所以关注第 n - 2 种有几种覆盖方法
总和为两种情况的总和
所以总结:f [n]表示2*n大矩阵 的方法数。
可以得出:f[n] = f[n-1] + f[n-2],初始条件f[1] = 1, f[2] =2
所以代码可用递归,记忆递归,和动态规划和递推
这里只写递推代码:
int rectCover(int number) {
if(0==number) return 0;
if(1==number) return 1;
if(2==number) return 2;
int a=1,b=2;
for(int i=3; i<=number; ++i){
b = a+b;
a = b-a;
}
return b;
}
二进制中1的个数
题目描述:
输入一个整数,输出该数32位二进制表示中1的个数。其中负数用补码表示。
int NumberOf1(int n) {
int count=0;
int key = 0x01;
while(key != 0 ){
if(n & key){
count++;
}
key <<= 1;
}
return count;
}
更好的办法
如果一个整数不为0,那么这个整数至少有一位是1。如果我们把这个整数减1,那么原来处在整数最右边的1就会变为0,原来在1后面的所有的0都会变成1(如果最右边的1后面还有0的话)。其余所有位将不会受到影响。
举个例子:一个二进制数1100,从右边数起第三位是处于最右边的一个1。减去1后,第三位变成0,它后面的两位0变成了1,而前面的1保持不变,因此得到的结果是1011.我们发现减1的结果是把最右边的一个1开始的所有位都取反了。这个时候如果我们再把原来的整数和减去1之后的结果做与运算,从原来整数最右边一个1那一位开始所有位都会变成0。如1100&1011=1000.也就是说,把一个整数减去1,再和原整数做与运算,会把该整数最右边一个1变成0.那么一个整数的二进制有多少个1,就可以进行多少次这样的操作。
int NumberOf1(int n) {
int ret=0;
while(n){
++ret;
n = n&(n-1);
}
return ret;
}
数值的整次方
题目描述:
给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。
保证base和exponent不同时为0
方法一:暴力方法
很显然就是n个b相乘。循环n次。
class Solution {
public:
double Power(double b, int n) {
if (n < 0) {
b = 1 / b;
n = -n;
}
double ret = 1.0;
for (int i=0; i<n; ++i) ret *= b;
return ret;
}
};
时间复杂度:O(n)
空间复杂度:O(1)
方法二:递归法(快速幂)
假设我们求 x 8 x^{8} x8 ,如果我们知道 x 4 x^{4} x4 ,那么 x 8 = ( x 4 ) 2 x^{8}=(x^4)^2 x8=(x4)2
但是还有个小问题,如果n是偶数,那么上述没问题。
如果n是奇数, x n = ( x n 2 ) 2 ∗ x x^n=(x^{\frac{n}{2}})^2*x xn=(x2n)2∗x, 比如 x 7 = ( x 3 ) 2 ∗ x x^{7}= (x^{3})^{2}*x x7=(x3)2∗x
class Solution {
public:
double q_power(double b, int n) {
if (n == 0) return 1.0;
double ret = q_power(b, n/2);
if (n&1) { // 奇数
return ret * ret * b;
}
else {
return ret * ret;
}
}
double Power(double b, int n) {
if (n < 0) {
b = 1 / b;
n = -n;
}
return q_power(b, n);
}
};
时间复杂度:O(logn),每次规模减少一半
空间复杂度:O(logn),递归栈,因为要记住logn个变量
方法三:非递归的快速幂
假设求
x
6
x^{6}
x6,已知6可以表示成二进制110
可以表示成
6
=
1
∗
2
2
+
1
∗
2
1
+
0
∗
2
0
6 = 1 * 2^{2} + 1 * 2^{1} +0*2^{0}
6=1∗22+1∗21+0∗20,所以
x
6
x^{6}
x6 可以表示成
x
6
=
x
0
∗
2
0
+
1
∗
2
1
+
1
∗
2
2
=
x
0
∗
x
1
∗
2
1
∗
x
1
∗
2
2
x^{6} = x^ {0*2^{0} + 1*2^{1} + 1*2^{2}} = x^{0} * x^{1*2^{1}}*x^{1*2^{2}}
x6=x0∗20+1∗21+1∗22=x0∗x1∗21∗x1∗22
所以,对于二进制数,遇到位数是1的就乘到答案中。
class Solution {
public:
double Power(double b, int n) {
if (n < 0) {
b = 1 / b;
n = -n;
}
double x = b; // 记录x^0, x^1, x^2 ...
double ret = 1.0;
while (n) {
if (n&1) {
ret *= x; // 二进制位数是1的,乘进答案。
}
x *= x;
n >>= 1;
}
return ret;
}
};
调整数组顺序使奇数位于偶数前面
题目描述
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
自己的
class Solution {
public:
void reOrderArray(vector<int> &array) {
vector<int> v1,v2;
for(size_t i=0;i<array.size();++i){
if(1 == array[i] % 2)//奇数
{
v1.push_back(array[i]);
}
else{
v2.push_back(array[i]);
}
}
v1.insert(v1.end(), v2.begin(),v2.end());
array = v1;
}
};
方法一:使用辅助数组
函数的类型为 void func_name(array&),想让我们不开辟额外数组来解决,使用in-place就地算法 。 但是如果空间要求不高的话,我们还是可以开挂的。也就是开辟个额外数组,接下来的做法就非常简单了,遍历一次数组,遇到奇数直接放入新开的数组中,再遍历一次数组,遇到偶数就继续放入新开的数组。最后再进行一次copy。
代码如下:
class Solution {
public:
void reOrderArray(vector<int> &array) {
vector<int> arr;
for (const int v : array) {
if (v&1) arr.push_back(v); // 奇数
}
for (const int v : array) {
if (!(v&1)) arr.push_back(v); // 偶数
}
copy(arr.begin(), arr.end(), array.begin());
}
};
时间复杂度:O(n)
空间复杂度:O(n)
方法二:in-place算法
如果不开辟额外数组该怎么做呢?
初始化操作:记录一个变量i表示已经将奇数放好的下一个位置,显然最开始i=0,表示还没有一个奇数放好。
j 表示数组的下标,初始值为0, 表示从下标0开始遍历。
- 如果遇到偶数,j++
- 如果遇到奇数,假设位置为j,就将此奇数插入到i所指的位置,然后i往后移动一个位置,在插入之前,显然会涉及到数据的移动,也就是将[i,j-1]整体往后移动。
- 直到整个数组遍历完毕,结束
class Solution {
public:
void reOrderArray(vector<int> &array) {
int i = 0;
for (int j=0; j<array.size(); ++j) {
if (array[j]&1) {
int tmp = array[j];
for (int k=j-1; k>=i; --k) {
array[k+1] = array[k];
}
array[i++] = tmp;
}
}
}
};
时间复杂度:O(n^ 2 ) , 假设数组中一般偶数在前,一半奇数在后,每次都要移动n/2个元素,是n^2/4
空间复杂度:O(1)
方法三:使用STL库函数stable_partition()
函数原型:
template< class BidirIt, class UnaryPredicate > BidirIt stable_partition( BidirIt first, BidirIt last, UnaryPredicate p );
第三个参数P可传入一个仿函数,函数指针,Lambda表达式,这里所示代码为Lambda表达式。
函数的意思是:对传入的区间[first, last)中的每个值进行P(value)判断,如果为真,就放入左边,并且保持稳定。
class Solution {
public:
void reOrderArray(vector<int> &array) {
stable_partition(array.begin(), array.end(), [](int x) {return x&1;} );
}
};
时间复杂度:O(n)
空间复杂度:O(n),内部使用了额外数组
链表中倒数第k个节点
题目描述:
输入一个链表,输出该链表中倒数第k个结点。
快慢指针
快指针先往前走k步,注意判断边界,然后快慢一起走,当快指针为none的时候,慢指针走到了倒数第k个节点。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
if(NULL == pListHead || k <= 0) return nullptr;
ListNode* p = pListHead;
ListNode* q = pListHead;
while(k--){
if(q)
q = q->next;
else
return nullptr;
}
while(q){
p = p->next;
q = q->next;
}
return p;
}
};
翻转链表
题目描述:
输入一个链表,反转链表后,输出新链表的表头。
自己的
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
if(NULL == pHead) return NULL;
ListNode* pre = pHead;
ListNode* cur = pre->next;
ListNode* nex;
while(cur){
nex = cur->next;
cur->next = pre;
pre = cur;
cur = nex;
}
pHead->next = NULL;
pHead = pre;
return pHead;
}
};
题解:
方法一:构造链表
如果此类型的题出现在笔试中,如果内存要求不高,可以采用如下方法:
可以先用一个vector将单链表的指针都存起来,然后再构造链表。
此方法简单易懂
ListNode* ReverseList(ListNode* pHead) {
if (!pHead) return nullptr;
vector<ListNode*> v;
while (pHead) {
v.push_back(pHead);
pHead = pHead->next;
}
reverse(v.begin(), v.end()); // 反转vector,也可以逆向遍历
ListNode *head = v[0];
ListNode *cur = head;
for (int i=1; i<v.size(); ++i) { // 构造链表
cur->next = v[i]; // 当前节点的下一个指针指向下一个节点
cur = cur->next; // 当前节点后移
}
cur->next = nullptr; // 切记最后一个节点的下一个指针指向nullptr
return head;
}
时间复杂度:O(n)
空间复杂度:O(n), 用了一个vector来存单链表
方法二:正规解法
但是面试的时候,上一种解法当然不行。此题想考察的是:如何调整链表指针,来达到反转链表的目的。
初始化:3个指针
1)pre指针指向已经反转好的链表的最后一个节点,最开始没有反转,所以指向nullptr
2)cur指针指向待反转链表的第一个节点,最开始第一个节点待反转,所以指向head
3)nex指针指向待反转链表的第二个节点,目的是保存链表,因为cur改变指向后,后面的链表则失效了,所以需要保存
接下来,循环执行以下三个操作
1)nex = cur->next, 保存作用
2)cur->next = pre 未反转链表的第一个节点的下个指针指向已反转链表的最后一个节点
3)pre = cur, cur = nex; 指针后移,操作下一个未反转链表的第一个节点
循环条件,当然是cur != nullptr
循环结束后,cur当然为nullptr,所以返回pre,即为反转后的头结点
ListNode* ReverseList(ListNode* pHead) {
ListNode *pre = nullptr;
ListNode *cur = pHead;
ListNode *nex = nullptr; // 这里可以指向nullptr,循环里面要重新指向
while (cur) {
nex = cur->next;
cur->next = pre;
pre = cur;
cur = nex;
}
return pre;
}
时间复杂度:O(n), 遍历一次链表
空间复杂度:O(1)
合并两个有序的链表
题目描述
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
自己的
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
ListNode *h1,*h2,*h3,*ret;
h1 = pHead1;
h2 = pHead2;
ret = h3 = NULL;
if(NULL == h1 && NULL == h2)
return NULL;
if(NULL == h1)
return h2;
if(NULL == h2)
return h1;
if(h1->val <= h2->val){
ret = h3 = h1;
h1 = h1->next;
}
else{
ret = h3 = h2;
h2 = h2->next;
}
while(h1 && h2){
if(h1->val <= h2->val){
h3->next = h1;
h3 = h3->next;
h1 = h1->next;
}
else{
h3->next = h2;
h3 = h3->next;
h2 = h2->next;
}
}
while(h1){
h3->next = h1;
h3 = h3->next;
h1 = h1->next;
}
while(h2){
h3->next = h2;
h3 = h3->next;
h2 = h2->next;
}
h3 = NULL;
return ret;
}
};
//或者
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
ListNode *h1,*h2,*h3,*ret;
h1 = pHead1;
h2 = pHead2;
ret = h3 = NULL;
if(NULL == h1 && NULL == h2)
return NULL;
if(NULL == h1)
return h2;
if(NULL == h2)
return h1;
if(h1->val <= h2->val){
ret = h3 = h1;
h1 = h1->next;
}
else{
ret = h3 = h2;
h2 = h2->next;
}
while(h1 && h2){
if(h1->val <= h2->val){
h3->next = h1;
h3 = h3->next;
h1 = h1->next;
}
else{
h3->next = h2;
h3 = h3->next;
h2 = h2->next;
}
}
if(h1){
h3->next = h1;
}
if(h2){
h3->next = h2;
}
return ret;
}
经典题解
public ListNode Merge(ListNode list1, ListNode list2) {
ListNode head = new ListNode(0);
ListNode movnode = head;
while (list1 != null && list2 != null) {
if (list1.val > list2.val) {
movnode.next = list2;
list2 = list2.next;
} else {
movnode.next = list1;
list1 = list1.next;
}
movnode = movnode.next;
}
if (list1 != null) {
movnode.next = list1;
}
if (list2 != null) {
movnode.next = list2;
}
return head.next;
}
二维数组中的查找
题目描述
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
示例1
输入:7, [[1,2,8,9],[2,4,9,12],[4,7,10,13],[6,8,11,15]]
返回值:true
分析:
假设arr数组,val,tar如下图所示:
如果我们把二分值定在右上角或者左下角,就可以进行二分。这里以右上角为例,左下角可自行分析:
图片说明
1)设初始值为右上角元素,val = arr[0][5] ,目标tar = arr[3][1]
2)接下来进行二分操作:
3)如果val == target,直接返回
4)如果 tar > val, 说明target在更大的位置,val左边的元素显然都是 < val,间接 < tar,说明第 0 行都是无效的,所以val下移到arr[1][5]
5)如果 tar < val, 说明target在更小的位置,val下边的元素显然都是 > val,间接 > tar,说明第 5 列都是无效的,所以val左移到arr[0][4]
6)继续步骤2)
复杂度分析
时间复杂度:O(m+n) ,其中m为行数,n为列数,最坏情况下,需要遍历m+n次。
空间复杂度:O(1)
bool Find(int target, vector<vector<int> > array) {
const int rows = array.size();
const int cols = array[0].size();
if(0==rows || 0==cols) return false;
int r,c;
r=0; c=cols-1;
while(r<rows && c>=0){
if(array[r][c] == target)
return true;
else if(array[r][c] < target)
++r;
else
--c;
}
return false;
}
树的子结构
题目描述
输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
class Solution {
bool dfs(TreeNode *r1, TreeNode *r2) {
if (!r2) return true;
if (!r1) return false;
return r1->val==r2->val && dfs(r1->left, r2->left) && dfs(r1->right, r2->right);
}
public:
bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2)
{
if(NULL == pRoot1 || NULL == pRoot2)
return false;
return dfs(pRoot1,pRoot2) || HasSubtree(pRoot1->left, pRoot2) ||
HasSubtree(pRoot1->right, pRoot2);
}
};
二叉树的镜像
题目描述
操作给定的二叉树,将其变换为源二叉树的镜像。
输入描述:
二叉树的镜像定义:
源二叉树
8
/ \
6 10
/ \ / \
5 7 9 11
镜像二叉树
8
/ \
10 6
/ \ / \
11 9 7 5
自己的
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
class Solution {
public:
void Mirror(TreeNode *pRoot) {
if(NULL == pRoot) return;
TreeNode *tmp = pRoot->left;
pRoot->left = pRoot->right;
pRoot->right = tmp;
Mirror(pRoot->left);
Mirror(pRoot->right);
}
};
牛客题解
题目抽象:给定一颗二叉树,将二叉树的左右孩子进行翻转,左右孩子的子树做相同的操作。
方法一:递归版本
根据题意,如果我们知道一个根节点的左孩子指针和右孩子指针,那么再改变根节点的指向即可解决问题。
也就是,需要先知道左右孩子指针,再处理根节点。显然对应遍历方式中的后序遍历。
- 后序遍历的模板:
void postOrder(TreeNode *root) {
if (!root) return;
postOrder(root->left); // left child
postOrder(root->right); // right child
// process root
}
class Solution {
public:
TreeNode* swapLR(TreeNode* r){
if(nullptr == r) return nullptr;
TreeNode* pLeftNode = swapLR(r->left);
TreeNode* pRightNode = swapLR(r->right);
r->left = pRightNode;
r->right = pLeftNode;
return r;
}
void Mirror(TreeNode *pRoot) {
if(NULL == pRoot) return;
swapLR(pRoot);
}
};
方法二:非递归版本
方法一种的递归版本中遍历树的方法用的是后序遍历。所以非递归版本,只需要模拟一次树遍历。
这里模拟树的层次遍历。
层次遍历的模板为:
void bfs(TreeNode *root) {
queue<TreeNode*> pq;
pq.push(root);
while (!pq.empty()) {
int sz = pq.size();
while (sz--) {
TreeNode *node = pq.front(); pq.pop();
// process node, ours tasks
// push value to queue
if (node->left) pq.push(node->left);
if (node->right) pq.push(node->right);
} // end inner while
} // end outer while
}
所以我们的代码为
class Solution {
public:
void Mirror(TreeNode *pRoot)
{
//传入头结点为空直接返回
if(!pRoot) return ;
queue<TreeNode *> que;
//在while操作之前把头结点加入队列,否则队列为空直接结束
que.push(pRoot);
while(!que.empty())
{
//获取每一层的结点数,即会对这一层的queSize个结点都执行交换操作
int queSize = que.size();
//处理完一个结点后,计数-1
while(queSize--)
{
//取得当前队列第一个元素(并非出队)
TreeNode *temp = que.front();
//出队
que.pop();
//先交换,再把其左右结点入队
//定义临时的TreeNode类型指针,用于交换左右结点
TreeNode *swap;
swap = temp->left;
temp->left = temp->right;
temp->right = swap;
//如果左(或右)结点不为空,就加入队列
if(temp->left) {que.push(temp->left);}
if(temp->right) {que.push(temp->right);}
}//继续对这一层的下一个结点进行操作
}//队列不空时,获取下一层结点的个数,并会执行相应次数的交换与入队操作
}
};
顺时针打印矩阵
题目描述
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.
示例1
输入
[[1,2],[3,4]]
返回值
[1,2,4,3]
题解:
vector<int> printMatrix(vector<vector<int> > matrix) {
vector<int> ret;
if (matrix.empty()) return ret;
int row = matrix.size();
int col = matrix[0].size();
int up = 0, down = row - 1, left = 0, right = col - 1;
while (true){
for (int j = left; j <= right; ++j)// 最上面一行
ret.push_back(matrix[up][j]);
++up;
if (up>down)
break;
for (int i = up; i <= down; ++i)
ret.push_back(matrix[i][right]);// 最右边一列
--right;
if (left>right)
break;
for (int j = right; j >= left; --j)// 最下面一行
ret.push_back(matrix[down][j]);
--down;
if (up>down)
break;
for (int i = down; i >= up; --i)// 最左边一列
ret.push_back(matrix[i][left]);
++left;
if (left>right)
break;
}
return ret;
}
包括min函数的栈
题目描述
定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。
题解
题目抽象:要求实现一个O(1)时间复杂度的返回最小值的栈。正常情况下,栈的push,pop操作都为O(1),
但是返回最小值,需要遍历整个栈,时间复杂度为O(n),所以这里需要空间换时间的思想。
方法:使用辅助栈
首先需要一个正常栈normal,用于栈的正常操作,然后需要一个辅助栈minval,专门用于获取最小值,具体操作如下。
class Solution {
public:
stack<int> normal, minval;
void push(int value) {
normal.push(value);
if (minval.empty()) {
minval.push(value);
}
else {
if (value <= minval.top()) {
minval.push(value);
}
else {
minval.push(minval.top());
}
}
}
void pop() {
normal.pop();
minval.pop();
}
int top() {
return normal.top();
}
int min() {
return minval.top();
}
};
栈的压人、弹出序列
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)
思路:新建一个栈,将数组A压入栈中,当栈顶元素等于数组B时,就将其出栈,当循环结束时,判断栈是否为空,若为空则返回true.
bool IsPopOrder(vector<int> pushV,vector<int> popV) {
if(pushV.size()==0 || popV.size()==0 || pushV.size()!=popV.size()){
return false;
}
stack<int> st;
int i=0, j=0;
while(i<pushV.size()){
if(pushV[i] != popV[j])
st.push(pushV[i++]);
else{
++i,++j;
while(!st.empty() && st.top()==popV[j]){
st.pop();
++j;
}
}
}
return st.empty();
}
层次遍历二叉树
题目描述
从上往下打印出二叉树的每个节点,同层节点从左至右打印。
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
class Solution {
public:
vector<int> PrintFromTopToBottom(TreeNode* root) {
vector<int> vRet;
int deep = 0; //深度
if(nullptr == root) return vRet;
queue<TreeNode*> q;
q.push(root);
while(!q.empty()){
int sz = q.size();
while(sz--){
TreeNode* node = q.front(); q.pop();
vRet.push_back(node->val);
if(node->left) q.push(node->left);
if(node->right) q.push(node->right);
}
++deep;
}
return vRet;
}
};
二叉搜索树的后序遍历序列
题目描述
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则返回true,否则返回false。假设输入的数组的任意两个数字都互不相同。(ps:我们约定空树不是二叉搜素树)
示例1
输入
[4,8,6,12,16,14,10]
返回值
true
方法一(递归)
class Solution {
bool judge(vector<int> a, int l, int r){
if(l >= r) return true;
int i=r;
while(i > l && a[i-1] > a[r]){
--i;
}
for(int j=i-1; j>=l; --j){
if(a[j] > a[r])
return false;
}
return judge(a, l, i-1) && (judge(a, i, r-1));
}
public:
bool VerifySquenceOfBST(vector<int> sequence) {
if(!sequence.size()) return false;
return judge(sequence, 0, sequence.size()-1);
}
};
方法二(非递归)
class Solution {
public:
bool VerifySquenceOfBST(vector<int> sequence) {
int size = sequence.size();
if(0 == size)
return false;
int i=0;
while(--size){
while(sequence[i] < sequence[size])
++i;
while(i<size && sequence[i] > sequence[size])
++i;
if(i < size)
return false;
i=0;
}
return true;
}
};
二叉树中和为某一值的路径
题目描述
输入一颗二叉树的根节点和一个整数,按字典序打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。
示例1
输入
{10,5,12,4,7},22
返回值
[[10,5,7],[10,12]]
示例2
输入
{10,5,12,4,7},15
返回值
[]
前置知识:
首先清楚叶子的表示:如果节点为root, 那么当前节点为叶子节点的必要条件为!root->left && !root->right
找出路径,当然需要遍历整棵树,这里采用先序遍历,即:根节点,左子树,右子树
代码如下:
void preOrder(TreeNode *root) {
// process root
if (root->left) preOrder(root->left);
if (root->right) preOrder(root->right);
}
具备了上面两个前置知识后,这里无非增加了路径和sum 和 叶子节点的判断。
- 递归法
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
class Solution {
using vvi = vector<vector<int>>;
using vi = vector<int>;
vi path;
vvi ret;
void dfs(TreeNode* root, int sum){
// 使用先序遍历
// 1. process root
if(NULL == root) return;
path.push_back(root->val);
if(!root->left && !root->right && sum == root->val)
ret.push_back(path);
// 2. 处理左子树
if(root->left)
dfs(root->left, sum - root->val);
// 3. 处理右子树
if(root->right)
dfs(root->right, sum - root->val);
path.pop_back();//清空栈,用来存放下一次的路径
}
public:
vector<vector<int> > FindPath(TreeNode* root,int expectNumber) {
dfs(root,expectNumber);
return ret;
}
};
JZ25 复杂链表的复制
题目大意:输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头节点
注意审题:我们需要对链表进行深拷贝
- 算法1(哈希表)
解题思路:
假如链表没有随机指针, 我们的拷贝方式很简单, 只要先创建一个哨兵节点, 然后遍历一下给定链表, 创建新节点, 并不断和上个节点连接。
本题的难点就在于每个节点还有一个指向空或其它节点的指针, 一种比较直观的想法就是先把整条链表连起来, 然后再挨着改变指针, 因此我们可以用哈希表来存放指针的映射关系, 然后根据将随机指针指向原链表随机指针映射在新链表的位置。
算法实现:首先创建一个哨兵节点, 遍历一次原链表, 先将除随机指针外的部分创建并连接, 同时用哈希表记录指针之间的映射, 最后遍历一次哈希表, 将随机指针指向对应的位置 。
代码实现(C++11)
/*
struct RandomListNode {
int label;
struct RandomListNode *next, *random;
RandomListNode(int x) :
label(x), next(NULL), random(NULL) {
}
};
*/
class Solution {
public:
RandomListNode* Clone(RandomListNode* pHead) {
if(nullptr == pHead) return nullptr;
map<RandomListNode*,RandomListNode*> m1;
RandomListNode* dummy = new RandomListNode(0);
RandomListNode* p = pHead;
RandomListNode* q = dummy;
while(p){
q->next = new RandomListNode(p->label);
//q->random = p->random;//不能使用原来的节点
m1.insert(make_pair(p,q->next));
q = q->next;
p = p->next;
}
for(auto& [key,value] : m1)
{
value->random = key->random == NULL? NULL : m1[key->random];
}
RandomListNode* res = dummy->next;
delete dummy;
return res;
}
};
//别人的
class Solution {
public:
RandomListNode* Clone(RandomListNode* pHead) {
if(!pHead) return pHead; // 为空则直接返回空
unordered_map<RandomListNode*, RandomListNode*> mp; // 创建哈希表
RandomListNode* dummy = new RandomListNode(0); // 哨兵节点
RandomListNode *pre = dummy, *cur = pHead; // 指向哨兵和链表头的指针
while(cur){
RandomListNode* clone = new RandomListNode(cur->label); // 拷贝节点
pre->next = clone; // 与上个结点连接
mp[cur] = clone; // 记录映射关系
pre = pre->next; // 指针移动
cur = cur->next;
}
for(auto& [key, value] : mp){ // 遍历哈希表
value->random = key->random == NULL ? NULL : mp[key->random];
}
delete dummy; // 释放哨兵节点空间
return mp[pHead];
}
};
时间复杂度:O(n), 遍历一次链表和哈希表的时间
空间复杂度:O(n), 哈希表使用的空间
-
算法2(链表拼接、拆分)
解题思路
此解法参考了大佬的做法, 主要思路是将原链表的结点对应的拷贝节点连在其后, 最后链表变成 原1 -> 拷1 -> 原2 -> 拷2 -> … -> null 的形式
然后我们再逐步处理对应的随机指针, 使用双指针, 一个指针指向原链表的节点, 一个指向拷贝链表的节点, 那么就有 拷->random = 原->random->next (random不为空)
最后再用双指针将两条链表拆分即可, 此算法大大优化了空间复杂度, 十分优秀
class Solution {
public:
RandomListNode* Clone(RandomListNode* pHead) {
if(!pHead) return pHead; // 为空则返回
RandomListNode* cur = pHead;
while(cur){
RandomListNode* tmp = new RandomListNode(cur->label); // 拷贝节点
tmp->next = cur->next;
cur->next = tmp;
cur = tmp->next;
}
RandomListNode *old = pHead, *clone = pHead->next, *ret = pHead->next;
while(old){
clone->random = old->random == NULL ? NULL : old->random->next; // 处理拷贝节点的随机指针
if(old->next) old = old->next->next; // 注意特判空指针
if(clone->next) clone = clone->next->next;
}
old = pHead, clone = pHead->next;
while(old){ // 拆分链表
if(old->next) old->next = old->next->next;
if(clone->next) clone->next = clone->next->next;
old = old->next;
clone = clone->next;
}
return ret;
}
};
JZ36 二叉搜索树与双向链表
我的
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
class Solution {
vector<TreeNode*> vt;
public:
TreeNode* Convert(TreeNode* root) {
if(NULL == root) return root;
//inOrder(root);
inorderTraversal(root);
for(int i=0; i<vt.size(); ++i)
{
TreeNode* pTmp = vt[i];
if(i>0)
pTmp->left = vt[i-1];
if(i<vt.size()-1)
pTmp->right = vt[i+1];
}
/*for (int i=0;i<vt.size()-1;i++){ //根据数组中的顺序将结点连接,注意i的范围。
vt[i]->right = vt[i+1];
vt[i+1]->left = vt[i];
}*/
return vt[0];
}
void inOrder(TreeNode* root){
if (!root) return;
inOrder(root->left); // left child
// process root
vt.push_back(root);
inOrder(root->right); // right child
}
//非递归的中序遍历
void inorderTraversal(TreeNode* root) {
std::stack<TreeNode*> st;
TreeNode* cur = root;
while (cur != NULL || !st.empty()) {
if (cur != NULL) { // 指针来访问节点,访问到最底层
st.push(cur); // 将访问的节点放进栈
cur = cur->left; // 左
}
else {
cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据)
st.pop();
vt.push_back(cur); // 中
cur = cur->right; // 右
}
}
}
};
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
class Solution {
public:
TreeNode* preNode;//preNode一定是全局变量。
TreeNode* Convert(TreeNode* pRootOfTree) {
if (!pRootOfTree) return pRootOfTree;
TreeNode* p = pRootOfTree;
while (p->left) p = p->left;//找到双向链表的开头。
inorder(pRootOfTree);
return p;
}
void inorder(TreeNode* root){
if (!root) return;
inorder(root->left);
//当前结点中需要进校的调整。
root->left = preNode;
if (preNode){
preNode->right = root;
}
preNode = root;//更新preNode,指向当前结点,作为下一个结点的前继。
inorder(root->right);
}
};
/*
时间复杂度:O(N),等于中序遍历的时间复杂度。
空间复杂度:O(N)。没有申请新的空间,但是递归调用栈占用了N的空间。
*/
JZ37 序列化二叉树
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};
*/
class Solution {
public:
char* Serialize(TreeNode *root) {
string s;
queue<TreeNode*> qt;
qt.push(root);
while (!qt.empty())
{
// pop operator
TreeNode *node = qt.front();
qt.pop();
// process null node
if (node == nullptr)
{
s.push_back('#');
s.push_back(',');
continue;
}
// process not null node
s += to_string(node->val);
s.push_back(',');
// push operator
qt.push(node->left);
qt.push(node->right);
}
char *ret = new char[s.length() + 1];
strcpy(ret, s.c_str());
return ret;
}
TreeNode* Deserialize(char *str) {
if (str == nullptr) {
return nullptr;
}
// 可用string成员函数
string s(str);
if (str[0] == '#') {
return nullptr;
}
// 构造头结点
queue<TreeNode*> nodes;
TreeNode *ret = new TreeNode(atoi(s.c_str()));
s = s.substr(s.find_first_of(',') + 1);
nodes.push(ret);
// 根据序列化字符串再层次遍历一遍,来构造树
while (!nodes.empty() && !s.empty())
{
TreeNode *node = nodes.front();
nodes.pop();
if (s[0] == '#')
{
node->left = nullptr;
s = s.substr(2);
}
else
{
node->left = new TreeNode(atoi(s.c_str()));
nodes.push(node->left);
s = s.substr(s.find_first_of(',') + 1);
}
if (s[0] == '#')
{
node->right = nullptr;
s = s.substr(2);
}
else
{
node->right = new TreeNode(atoi(s.c_str()));
nodes.push(node->right);
s = s.substr(s.find_first_of(',') + 1);
}
}
return ret;
}
};