0424
剑指offer读书笔记2
面试题1-20
第3章 高质量的代码
3.3 代码的完整性
面试题21:调整数组顺序使奇数位于偶数前面
笨方法就是创建一个vector数组,然后遍历两次原来的数组,第一次把奇数尾插,第二次把偶数尾插,就是答案。
书上的方法是首尾双指针法:
- 头指针往后移,直到遇到偶数;
- 尾指针往前移,直到遇到奇数;
- 同时满足上面两种情况,就把二者指向的内容交换,
- 然后重复步骤1-3,直到首位相碰
根据思路自己写的:(在acwing上能够,在力扣上过不了,力扣的数据范围大的多)
class Solution {
public:
void reOrderArray(vector<int> &array) {
if(array.size() == 0) return;
int left = 0;
int right = array.size() - 1;
while(left < right){
while(array[left] % 2 == 1) ++left;
while(array[right] % 2 == 0) --right;
if(left < right)
swap(array[left], array[right]);
}
}
};
上面的写法过不了力扣,因为如果是全奇数{1,3,5,7},left
会直接到array.size()
的位置,这就已经超出下标范围了!!!
所以解决办法就是在内层的两个while
循环中,加上一个left < right
,就可以了:
代码:
(首尾双指针法)
class Solution {
public:
class Solution {
public:
vector<int> exchange(vector<int>& nums) {
int l = 0;
int r = nums.size() - 1;
while(l < r){
while(l < r && (nums[l] % 2) == 1) ++l;
while(l < r && (nums[r] % 2) == 0) --r;
//if(l < r){
swap(nums[l], nums[r]);
//++l; -- r;
//}
}
return nums;
}
};
};
注意:上面代码中屏蔽掉的三行可有可无。
再附上一个(笨方法):
//acwing:
class Solution {
public:
void reOrderArray(vector<int> &array) {
vector<int> tmp;
//把原来的容器复制一下:
for(int i = 0; i < array.size(); i++) tmp.push_back(array[i]);
//记得把原来的容器清空:
array.erase(array.begin(),array.end());
//取奇数:
for(int i = 0; i < tmp.size(); i++){
if(tmp[i] % 2 == 1)
array.push_back(tmp[i]);
}
//取偶数:
for(int i = 0; i < tmp.size(); i++){
if(tmp[i] % 2 == 0)
array.push_back(tmp[i]);
}
}
};
//力扣
class Solution {
public:
vector<int> exchange(vector<int>& nums) {
vector<int> res;
for(int i = 0; i < nums.size(); i++){
if(nums[i] % 2 == 1)
res.push_back(nums[i]);
}
for(int i = 0; i < nums.size(); i++){
if(nums[i] % 2 == 0)
res.push_back(nums[i]);
}
return res;
}
};
还有一种快慢指针法,点这里。
面试题21题–>拓展
如果把题目改成
把数组中的数按照大小分为两部分,所有负数都在非负数的前面;
把数组中的数分为两部分,能被3整除的数都在不能被3整除的数的前面;
//所有奇数都在偶数的前面:
class Solution {
public:
vector<int> exchange(vector<int>& nums) {
//if(array.size() == 0) return;
int l = 0, r = nums.size() - 1;
while(l < r){
while(l < r && (nums[l] & 1) == 1) ++l;
while(l < r && (nums[r] & 1) == 0) --r;
if(l < r){
swap(nums[l], nums[r]);
++l; --r;
}
}
return nums;
}
};
//所有负数都在非负数的面前:
class Solution1 {
public:
vector<int> exchange(vector<int>& nums) {
//if(array.size() == 0) return;
int l = 0, r = nums.size() - 1;
int count = 0;
while(l < r){
while(l < r && nums[l] < 0) ++l;
while(l < r && nums[r] > 0) --r;
if(l < r){
swap(nums[l], nums[r]);
++l; --r;
}
}
return nums;
}
};
//所有能被3整除的数都在不能被3整除的数的前面:
class Solution2 {
public:
vector<int> exchange(vector<int>& nums) {
//if(array.size() == 0) return;
int l = 0, r = nums.size() - 1;
int count = 0;
while(l < r){
while(l < r && (nums[l] % 3) == 0) ++l;
while(l < r && (nums[r] % 3) != 0) --r;
if(l < r){
swap(nums[l], nums[r]);
++l; --r;
}
}
return nums;
}
};
3.4 代码的鲁棒性(Robust)
面试题22:链表中倒数第k个节点
笨方法:遍历两次链表,第一次求节点个数n,第二次遍历n-k+1次,就是要求的倒数第k个节点。
还有一次遍历就能完成的快慢指针法:快慢指针之间相差k,然后快指针到最后一个节点,慢指针就是要求的倒数第k个节点。
代码1(笨方法):
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
//笨方法:
int count = 0;
ListNode* p = head;
while(p){
++count;
p = p->next;
}
//如果k大于节点个数,返回空
if(k > count){
return NULL;
}
p = head;
//假如count为7,那么倒数第3个节点就是正着数第7-3+1个节点,那么从head出发往后走7-3步,就走到了正着数第7-3+1个节点了
for(int i = 1; i <= count - k; i++){
p = p->next;
}
return p;
}
};
关键在于判断k是否大于count。
代码2(快慢指针法):
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
//快慢指针法:
ListNode* slow = head;
ListNode* fast = head;
for(int i = 1; i <= k; i++){
if(fast)
fast = fast->next;
else//一旦fast为空,就说明k大于链表长度
return NULL;
//上面四行可以写成下面三行:
//if(fast == nullptr)
// return nullptr;
//fast = fast->next;
}
while(fast){
slow = slow->next;
fast = fast->next;
}
return slow;
}
};
关键在于判断fast指针是否已经到链表的末尾了。
题目拓展:
求链表中的中间节点,也可以用快慢指针法来解决,慢指针一次走一步,快指针一次走两步,快指针到链表末端,慢指针就恰好在链表的中间。
面试题23:链表中环的入口节点
这道题在力扣的剑指offer中没有,但是其实就是力扣的142:环形链表II,如果用下面的三步法的话,其中的第一步就是力扣的141. 环形链表)。
思路1:三步法,其实是双指针法,效率高,但思路太繁琐了,可以看下面的升级版的思路1;
思路2:哈希set实现,和141.环形链表的思路一样,只不过一个返回的是结点
,一个返回的是bool
,每次先查询当前节点是否出现过,如果出现过既能说明链表有环,而且此节点还是环的入口结点。特点是效率不高,但代码很少;
☆升级版的思路1见下面一段,应该是效率最高的方法了。
升级版的思路1:(用两次双指针法)
假设从head结点开始到环的入口结点一共是a
个结点(不算入口结点),环的结点个数为b
,那么从head
开始走a+n*b
步一定是环的入口结点;
然后用快慢指针fast
和slow
,fast
每次走两步,slow
每次走一步,当二者相遇时fast走的步数是slow的两倍,而且fast走的步数等于slow走的步数+n倍的环的结点个数,即它们走的步数 f = 2*s, f = s+n*b
,综合一下可得s=n*b
,也就是说当fast
和slow
第一次相遇时,slow
走的步数等于n
倍的环的个数n*b
;
此时slow
只需再走a
步就可以到环的入口,可是我们并不知道a
是多少,但我们知道从head
开始走a
步可以到环的入口,而slow
走a
步也正好到环的入口,所以这时再用一次双指针,让临时tmp
结点指向head
,然后tmp
和slow
同步走,当二者相遇时,tmp
和slow
都走了a
步,所处的位置正好也是环的入口。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
//双指针法:
ListNode* slow = head;
ListNode* fast = head;
while(fast){
slow = slow->next;
fast = fast->next;
if(fast){//fast非空再走第二步:
fast = fast->next;
if(fast == slow){//slow和fast第一次相遇,说明一定有环
//此时开始让tmp结点从head开始走,slow也同步走
ListNode* tmp = head;
while(tmp != slow){
tmp = tmp->next;
slow = slow->next;
}
//当tmp和head相遇时,就是环的入口结点
return slow;
}
}
//else//fast为空,就说不存在环,直接返回null
// return NULL;
}
return NULL;
}
};
思路1:三步法
第一步是确定有没有环?
- 快慢指针法:慢指针一次走一步,快指针一次走两步,如果快慢指针相遇,就一定有环。
第二步是如何得到环中节点的数目?
- 前提是有环,即快慢指针会相遇,从相遇的地方开始继续走,边走边计数,直到回到原地,就能知道环内节点的数目了。
第三步是如何找到环的入口?
- 又是快慢指针,快慢指针之间相隔第2步计算出来的节点数目N,快指针先走N步,然后快慢指针同步走,直到快慢指针相遇,就是环的入口。
(5.27
又写了一遍,这道题的第一步是力扣上的141. 环形链表)
代码:
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead) {
//1.先判断是否有环
ListNode* slow = pHead;
ListNode* fast = pHead;
while(fast){
slow = slow->next;
fast = fast->next;
if(fast){// && fast->next
fast = fast->next;
if(fast == slow)
break;
}
}
if(fast == nullptr) return nullptr;//没有环
//fast非空就表示有环,然后计算环的结点个数
//2.计算环的结点个数
ListNode* cur = slow;
int count = 1;//slow结点算一个结点
slow = slow->next;
while(slow != fast){
++count;
slow = slow->next;
}
//3.找到换的入口
//快指针先走count步,然后快慢指针同步走,当二者相遇时,就是环的入口
slow = pHead;
fast = pHead;
for(int i = 0; i < count; ++i){
fast = fast->next;
}
while(slow != fast){
slow = slow->next;
fast = fast->next;
}
return slow;//->val
}
};
(下面是之前写的代码,上面是5.27又做一遍的时候写的)
代码:
(牛客上写的,下面又在acwing上写了一遍,可以看一下)
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};
*/
class Solution {
public:
ListNode* EntryNodeOfLoop(ListNode* pHead) {
ListNode* slow = pHead;
ListNode* fast = pHead;
bool flag = false;
//是否有环?
//一个结点构不成环:如果至少有一个结点,可以写下面这两句,但如果pHead可能是空,就不能写了
//if(fast->next == nullptr)
// return nullptr;
while(fast){
slow = slow->next;
fast = fast->next;
if(fast)//->next
fast = fast->next;
if(slow == fast && slow != nullptr){//快慢指针相遇且不为空
flag = true;
break;
}
}
//计算环内的结点个数:
ListNode* start = nullptr;
int count = 0;
if(flag){
start = slow;
slow = slow->next;
++count;
while(start != slow){
slow = slow->next;
++count;
}
}
else//不构成环
return nullptr;
//找环的入口:
slow = pHead;
fast = pHead;
for(int i = 1; i <= count; i++) fast = fast->next;
while(fast != slow){
slow = slow->next;
fast = fast->next;
}
return slow;
}
};
(acwing上又写了一遍)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *entryNodeOfLoop(ListNode *head) {
ListNode* slow = head;
ListNode* fast = head;
//1.先判断有没有环:
bool flag = false;
//一个结点构不成环:如果至少有一个结点,可以写下面这两句,但如果pHead可能是空,就不能写了
//if(fast->next == nullptr)
// return nullptr;
while(fast){
slow = slow->next;
fast = fast->next;
if(fast)
fast = fast->next;
if(slow == fast && fast != NULL){//一个结点构不成环
flag = true;
break;
}
}
ListNode* begin = NULL;
if(flag){
begin = slow;
}
else
return NULL;
//2.计算环内结点个数:
int count = 0;
do{
slow = slow->next;
++count;
}while(begin != slow);
//3.找到环的入口:
slow = head;
fast = head;
for(int i = 1; i <= count; i++){
fast = fast->next;
}
while(slow != fast){
slow = slow->next;
fast = fast->next;
}
return slow;
}
};
思路2:哈希表
用哈希set实现:
先判断结点是否出现过,如果出现过那么就是环的入口结点;如果没出现过就插入进来。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
//哈希表法:
unordered_set<ListNode*> hashSet;
ListNode* cur = head;
while(cur){
if(hashSet.find(cur) != hashSet.end()) return cur;
hashSet.insert(cur);
cur = cur->next;
}
return NULL;
}
};
面试题24:反转链表
相关题目 --> 面试题6:从尾到头打印链表
笨方法:用一个栈记录链表的内容,然后出栈,重新建一个链表。
头插法:创建一个虚拟头结点preHead,然后遍历原来的链表,把结点挨个头插到虚拟头结点的后面。
其他方法:(双指针法、递归)
【反转链表】:双指针,递归,妖魔化的双指针
代码:(笨方法 + 头插法)
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
//笨方法:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head == NULL) return NULL;
//栈:
stack<int> st;
while(head){
st.push(head->val);
head = head->next;
}
ListNode* preHead = new ListNode(0);
ListNode* p = preHead;
while(!st.empty()){
ListNode* tmp = new ListNode(st.top());
st.pop();
//tmp->next = NULL;
p->next = tmp;
p = p->next;
}
return preHead->next;
}
};
//头插法:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head == NULL) return NULL;
ListNode* preHead = new ListNode(0);
while(head){
ListNode* p = head->next;
head->next = preHead->next;
preHead->next = head;
head = p;
}
return preHead->next;
}
};
代码:(其他方法:双指针法、递归)
//双指针法:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head == NULL) return NULL;
ListNode* pre = head;
ListNode* cur = NULL;
while(pre){
ListNode* p = pre->next;
pre->next = cur;
cur = pre;
pre = p;
}
return cur;
}
};
//递归法:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
//终止条件:
if(head == NULL || head->next == NULL) {
return head;
}
//递归操作:
ListNode* node = reverseList(head->next);
head->next->next = head;
head->next = NULL;
//返回值:
return node;
}
};
补充:
上面的其他方法的链接下面评论中有个关于递归的总结,我复制下来了:
我自己总结的思路,不管这个我解决递归的思路是错是对,都挺愿意能让大家看到。这个思路对写递归代码来说,还是比较简单适用的,比我看到的这些把递归掰开来揉碎了来讲的要更容易出代码。希望对写递归还比较迷茫的同学们有所帮助。
先放结论 Rules Number One,基本上,所有的递归问题都可以用递推公式来表示。有了这个递推公式,我们就可以很轻松地将它改为递归代码。。所以,遇到递归不要怕,先想递推公式。
例1: (比较明显的能递推公式的问题)
问题:斐波那契数列的第n项
递推公式:
f(n)=f(n-1)+f(n-2) 其中,f(0)=0,f(1)=1
终止条件:
if (n <= 2) return 1;
递归代码:
int f(int n) {
if (n <= 2) return 1;
return f(n-1) + f(n-2);
}
例2:(不那么明显的有递推公式的问题)
问题:逆序打印一个数组
递推公式:
假设令F(n)=逆序遍历长度为n的数组
那么F(n)= 打印数组中下标为n的元素 + F(n-1)
终止条件:
if (n < 0) return ;
递归代码:
public void Print(int[] nums,int n){
if(n<0) return;
System.out.println(nums[n]);
Print(nums,n-1);
}
到这里,不知道大家对写递归有没有一些理解了。其实写递归不能总想着去把递归平铺展开,这样脑子里就会循环,一层一层往下调,然后再一层一层返回,试图想搞清楚计算机每一步都是怎么执行的,这样就很容易被绕进去。对于递归代码,这种试图想清楚整个递和归过程的做法,实际上是进入了一个思维误区。只要找到递推公式,我们就能很轻松地写出递归代码。
到这里,我想进一步跟大家说明我这个思路是比较能够容易出代码的,那么就树的遍历问题来和大家讲。递归总是和树分不开,其中,最典型的便是树的遍历问题。刚开始学的时候,不知道大家是怎么理解先/中/后序遍历的递归写法的,这里我提供我的思路供参考,以前序遍历为例:
问题:二叉树的先序遍历
递推公式:
令F(Root)为问题:遍历以Root为根节点的二叉树,
令F(Root.left)为问题:遍历以F(Root.left)为根节点的二叉树
令F(Root.right)为问题:遍历以F(Root.right)为根节点的二叉树
那么其递推公式为:
F(Root)=遍历Root节点+F(Root.left)+F(Root.right)
递归代码:
public void preOrder(TreeNode node){
if(node==null) return;
System.out.println(node.val);
preOrder(node.left);
preOrder(node.righr);
}
Rules Number Two, 递归是一种关于某个重复动作(完成重复性的功能)的形式化描述。具体点讲,如果一个问题 A 可以分解为若干子问题 B、C、D,你可以假设子问题 B、C、D 已经解决,在此基础上思考如何解决问题 A。而且,你只需要思考问题 A 与子问题 B、C、D 两层之间的关系即可,不需要一层一层往下思考子问题与子子问题,子子问题与子子子问题之间的关系(也就是说,递归只能考虑当前层和下一层的关系,不能继续往下深入)。我们需要屏蔽掉递归细节,理解为完成了某种功能的形式化描述即可。
好了,那我们来分析这个题。
问题:单向链表的反转
递推公式:
令F(node)为问题:反转以node为头节点的单向链表;
一般,我们需要考虑F(n)和F(n-1)的关系,那么这里,如果n代表以node为头节点的单向链表,那么n-1就代表以node.next为头节点的单向链表.
所以,我们令F(node.next)为问题:反转以node.next为头节点的单向链表;
那么,F(node)和F(node.next)之间的关系是?这里我们来简单画个图,假设我们反转3个节点的链表:
1 -> 2 -> 3
那么,F(node=1)=F(node=2)+?
这里假设子问题F(node=2)已经解决,那么我们如何解决F(node=1):
很明显,我们需要反转node=2和node=1, 即 node.next.next=node;
同时 node.next=null;
所以,这个问题就可以是:F(node=1) = F(node=2) + 反转node=2和node=1
递归代码:
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null) { //终止条件并不难想
return head;
}
ListNode node = reverseList(head.next);
head.next.next = head;
head.next = null;
return node; //按上面的例子,F(node=1)和F(node=2)它俩反转后的头节点是同一个
}
面试题25:合并两个排序的链表
笨方法:新创建一个虚拟头结点,遍历两个链表,用尾插法把结点按顺序创建新链表。
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* merge(ListNode* l1, ListNode* l2) {
ListNode* preHead = new ListNode(0);
ListNode* p = preHead;
while(l1 && l2){
if(l1->val < l2->val){
p->next = l1;
p = l1;
l1 = l1->next;
}
else{
p->next = l2;
p = l2;
l2 = l2->next;
}
}
if(l1){
p->next = l1;
}
if(l2){
p->next = l2;
}
return preHead->next;
}
};
面试题26:树的子结构
要查找树A中是否存在和树B结构一样的子树,我们可以分成两步:
- 在树A中找到和树B的根节点的val值一样的节点R,也就是树的遍历;
- 判断A中以R为根节点的(左右)子树是不是包含和树B一样的结构。
具体分析:
第一步中会用到递归,去实现树A的遍历:
- 如果A为空,肯定不匹配,返回false;
- 如果B为空,也不匹配,返回false;
- 然后是A和B都不为空,用A去和B匹配(下面的第二步):
①如果匹配成功,就返回true
;
②如果匹配失败,就通过递归进入A的左右子树去找,直到遇到A的叶节点,只要有一次匹配成功,就可以了,所以是或||
的关系。
第二步中也会用到递归,去实现A和B的匹配:
- 如果B为空,说明B前面的内容都匹配成功了,返回
true
; - 如果A为空,匹配失败,返回false;
- 然后是A和B都不会空,就判断A的val值和B的val值是否相等:
①如果不相等,说明匹配失败,返回false;
②如果相等,就通过递归同时进入A的左子树和B的左子树,然后同时进入A的右子树和B的右子树,直到遇到A的叶节点或B的叶节点;必须是左右子树都匹配才能说明匹配成功,所以是与&&
的关系。
再用上面的分析方式去分析下二叉树的前序遍历中怎么实现递归:
先判断root结点是不是空:
- 如果是空,就返回;
- 如果不是空,就把root的val值存到vector容器中,然后通过递归进入到root的左右子树,直到遍历到root为空,即遇到叶子结点。
小结:
树的递归中,
递归停止条件:遇到叶节点,即root为空;
递归的操作:如果还没遇到叶节点,就通过递归去遍历当前节点的孩子节点;
递归的返回值:一般用res来接收递归的操作的结果,最后return res;
。
再用上面的分析方式去分析下面试题24:反转链表中怎么实现递归:
(没分析明白[捂脸])
第一步:先判断head是否为空,
+ 如果为空,就返回null;
+ 如果非空,就进入下一步
第二步:判断head->next是否为空,即head是否是唯一的一个节点,同时也是判断head是否是最后一个结点
+ 如果是,就说明不需要反转,直接返回head即可;
+ 如果不是,就说明有两个和两个以上的结果,需要反转,就进入下一步
第三步:通过**递归**去找下一个结点,
通过递归去遍历head的下一个结点,直到链表的最后一个节点,然后返回head,用node接收,
代码:(二叉树的前序遍历)
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
if(root == nullptr) return res;
recur(root, res);
return res;
}
void recur(TreeNode* root, vector<int>& res){
//先判断root结点是不是空:
//如果是空,就返回;
if(root == nullptr) return;
//如果不是空,就把root的val值存到vector容器中,然后通过递归进入到root的左右子树,直到遍历到root为空,即遇到叶子结点。
res.push_back(root->val);
recur(root->left, res);
recur(root->right, res);
}
};
代码:(面试题26:树的子结构)
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool isSubStructure(TreeNode* A, TreeNode* B) {
//如果A为空,肯定不匹配,返回false;
if(A == NULL) return false;
//如果B为空,也不匹配,返回false;
if(B == NULL) return false;
//然后是A和B都不为空,用A去和B匹配(下面的isPart()函数):
//①如果匹配成功,就返回true;
if(isPart(A, B))
return true;
//如果匹配失败,就通过递归进入A的左右子树去找,直到遇到A的叶节点,只要有一次匹配成功,就可以了,所以是或||的关系。
else
return isSubStructure(A->left, B) || isSubStructure(A->right, B);
}
bool isPart(TreeNode* p1, TreeNode* p2){
//如果B为空,说明B前面的内容都匹配成功了,返回true;
if(p2 == NULL) return true;
//如果A为空,匹配失败,返回false;
if(p1 == NULL) return false;
//然后是A和B都不会空,就判断A的val值和B的val值是否相等:
//①如果不相等,说明匹配失败,返回false;
if(p1->val != p2->val)
return false;
//②如果相等,就通过递归同时进入A的左子树和B的左子树,然后同时进入A的右子树和B的右子树,直到遇到A的叶节点或B的叶节点;必须是左右子树都匹配才能说明匹配成功,所以是与&&的关系。
else
return isPart(p1->left, p2->left) && isPart(p1->right, p2->right);
}
};
下面是在力扣上找的有注释的代码,可以看看,帮助理解:
class Solution {
/*
参考:数据结构与算法的题解比较好懂
死死记住isSubStructure()的定义:判断B是否为A的子结构
*/
public boolean isSubStructure(TreeNode A, TreeNode B) {
// 若A与B其中一个为空,立即返回false
//这个函数对这棵树进行前序遍历:即处理根节点,再递归左子节点,再递归处理右子节点
//特殊情况是:当A或B是空树的时候 返回false
//用||关系可以达到 不同顺序遍历的作用
if(A == null || B == null) {
return false;
}
// B为A的子结构有3种情况,满足任意一种即可:
// 1.B的子结构起点为A的根节点,此时结果为recur(A,B)
// 2.B的子结构起点隐藏在A的左子树中,而不是直接为A的根节点,此时结果为isSubStructure(A.left, B)
// 3.B的子结构起点隐藏在A的右子树中,此时结果为isSubStructure(A.right, B)
return recur(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B);
}
/*
判断B是否为A的子结构,其中B子结构的起点为A的根节点
*/
//此函数的作用是从上个函数得到的根节点开始递归比较 是否是子树
private boolean recur(TreeNode A, TreeNode B) {
// 若B走完了,说明查找完毕,B为A的子结构
//结束条件
//当最后一层B已经为空的,证明则B中节点全是A中节点
if(B == null) {
return true;
}
// 若B不为空并且A为空或者A与B的值不相等,直接可以判断B不是A的子结构
if(A == null || A.val != B.val) {
return false;
}
//这里因为有上一个条件,则说明 A已经为空了,B却不为空,则一定不是子数
if(A==null){
return false;
}
//处理本次递归,即处理当前节点
if(A.val!=B.val){
return false;
}
// 当A与B当前节点值相等,若要判断B为A的子结构
// 还需要判断B的左子树是否为A左子树的子结构 && B的右子树是否为A右子树的子结构
// 若两者都满足就说明B是A的子结构,并且该子结构以A根节点为起点
//递归,同时递归左右两个子节点
return recur(A.left, B.left) && recur(A.right, B.right);
}
}
补充:
一篇文章带你吃透对称性递归(思路分析+解题模板+案例解读)
第4章 结局面试题的思路
4.2 画图让抽象问题形象化
面试题27:二叉树的镜像
思路:除了根节点不动,每个节点的左右孩子进行交换。其实就是二叉树的遍历,之前是把每个结点的值存到vector容器中,现在是把每个结点的左右孩子进行交换,其他都一样:
代码(二叉树的镜像):
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
//力扣:
class Solution {
public:
TreeNode* mirrorTree(TreeNode* root) {
if(root == NULL) return NULL;
recur(root);
return root;
}
void recur(TreeNode* node){
//先判断root结点是不是空:
//如果是空,就返回;
if(node == NULL) return;
//如果不是空,就把root的左右孩子进行交换,然后通过递归进入到root的左右子树,直到遍历到root为空,即遇到叶子结点。
TreeNode* tmp = node->left;
node->left = node->right;
node->right = tmp;
recur(node->left);
recur(node->right);
}
};
//力扣也可以这样写:
class Solution {
public:
TreeNode* mirrorTree(TreeNode* root) {
if(root == NULL) return NULL;
TreeNode* tmp = root->left;
root->left = root->right;
root->right = tmp;
mirrorTree(root->left);
mirrorTree(root->right);
return root;
}
};
//acwing:
class Solution {
public:
void mirror(TreeNode* root) {
if(root == NULL) return;
TreeNode* tmp = root->left;
root->left = root->right;
root->right = tmp;
mirror(root->left);
mirror(root->right);
}
};
代码(二叉树的遍历),可以和二叉树的镜像对比着看一下,基本都相似:
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
if(root == nullptr) return res;
recur(root, res);
return res;
}
void recur(TreeNode* root, vector<int>& res){
//先判断root结点是不是空:
//如果是空,就返回;
if(root == nullptr) return;
//如果不是空,就把root的val值存到vector容器中,然后通过递归进入到root的左右子树,直到遍历到root为空,即遇到叶子结点。
res.push_back(root->val);
recur(root->left, res);
recur(root->right, res);
}
};
面试题28:对称的二叉树 --> 面试题26:树的子结构
思路:
对称二叉树定义: 对于树中任意两个对称节点 L 和 R ,一定有:
L.val = R.val
:即此两对称节点值相等。L.left.val = R.right.val
:即 L 的 左子节点 和 R 的 右子节点 对称;L.right.val = R.left.val
:即 L 的 右子节点 和 R 的 左子节点 对称。
根据以上规律,考虑从顶至底递归,判断每对节点是否对称,从而判断树是否为对称二叉树。
如果根节点为空,返回true;
如果根节点非空,就对比其左右子树:
- 左右子树都为空,返回
true
; - 左右子树有一个为空,返回false;
- 左右子树都不为空,就判断它们的值是否相同:
如果不同,就返回false;
如果相同,就递归进入各自的左子树和右子树,只有 A的左子节点=B的右子节点&&
A的右子节点=B的左子节点,才算对称。
代码:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if(root == NULL) return true;
bool res = recur(root->left, root->right);
return res;
}
bool recur(TreeNode* node1, TreeNode* node2){
if(node1 == NULL && node2 == NULL) return true;
if(node1 == NULL || node2 == NULL) return false;
if(node1->val != node2->val)
return false;
else
return recur(node1->left, node2->right) && recur(node1->right, node2->left);//
}
};
注意:
- 最后一步是A的左子节点=B的右子节点
&&
A的右子节点=B的左子节点,才算对称。 - 面试题26:树的子结构的最后一步是A的左子节点=B的左子节点
&&
A的右子节点=B的右子节点,才算匹配成功。
这个题和面试题26:树的子结构有点相似,都是判断两个树之间的关系,这里附上第26题的代码,对比着这题的代码看下:
代码(面试题26:树的子结构):
class Solution {
public:
bool isSubStructure(TreeNode* A, TreeNode* B) {
//如果A为空,肯定不匹配,返回false;
if(A == NULL) return false;
//如果B为空,也不匹配,返回false;
if(B == NULL) return false;
//然后是A和B都不为空,用A去和B匹配(下面的isPart()函数):
//①如果匹配成功,就返回true;
if(isPart(A, B))
return true;
//如果匹配失败,就通过递归进入A的左右子树去找,直到遇到A的叶节点,只要有一次匹配成功,就可以了,所以是或||的关系。
else
return isSubStructure(A->left, B) || isSubStructure(A->right, B);
}
bool isPart(TreeNode* p1, TreeNode* p2){
//如果B为空,说明B前面的内容都匹配成功了,返回true;
if(p2 == NULL) return true;
//如果A为空,匹配失败,返回false;
if(p1 == NULL) return false;
//然后是A和B都不会空,就判断A的val值和B的val值是否相等:
//①如果不相等,说明匹配失败,返回false;
if(p1->val != p2->val)
return false;
//②如果相等,就通过递归同时进入A的左子树和B的左子树,然后同时进入A的右子树和B的右子树,直到遇到A的叶节点或B的叶节点;必须是左右子树都匹配才能说明匹配成功,所以是与&&的关系。
else
return isPart(p1->left, p2->left) && isPart(p1->right, p2->right);
}
};
面试题29:顺时针打印矩阵
这道题和力扣第54题相同,还有个类似的题是力扣第59题:螺旋矩阵II。
从左到右:
从上到下:
从右到左:
从下到上:
while循环写成while(true)
,记得在每个方向的遍历结束后判断是否越界,一旦越界就break;
退出循环。
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> res;
//如果二维数组有可能为空的话,就要判断 行为空 || 列为空,只判断 行为空 不够严谨,如果输入一个1行0列的,就会出错!!!
if(matrix.size() == 0 || matrix[0].size() == 0) return res;
//或者可以写成这样:
//if(matrix.empty() || matrix[0].empty()) return res;
int up = 0, down = matrix.size() - 1;//行
int left = 0, right = matrix[0].size() - 1;//列
int i;
while(true){
//从左到右:
for(i = left; i <= right; ++i){
res.push_back(matrix[up][i]);
}
++up;
if(up > down) break;
//从上到下:
for(i = up; i <= down; ++i){
res.push_back(matrix[i][right]);
}
--right;
if(right < left) break;
//从右到左:
for(i = right; i >= left; --i){
res.push_back(matrix[down][i]);
}
--down;
if(down < up) break;
//从下到上:
for(i = down; i >= up; --i){
res.push_back(matrix[i][left]);
}
++left;
if(left > right) break;
}
return res;
}
};
4.3 举例让抽象问题具体化
面试题30:包含min函数的栈
数据栈A:
- 栈 A 用于存储所有元素,保证入栈
push()
函数、出栈pop()
函数、获取栈顶top()
函数的正常逻辑;
辅助站B:
- 栈 B 中存储栈 A 中所有 非严格降序 的元素,则栈 A 中的最小元素始终对应栈 B 的栈顶元素,即
min()
函数只需返回栈 B 的栈顶元素即可。
注意:
1.入栈的时候注意先看st2
是否为空,如果为空就直接入栈,如果非空再通过比较把最小值入栈;
2.答案中在构造函数中把栈清空,力扣上构造函数中不写也能过。
代码:
class MinStack {
stack<int> st1;
stack<int> st2;
public:
/** initialize your data structure here. */
MinStack() {
//构造函数清空栈容器:
while(!st.empty()) {
st.pop();
}
while(!minStack.empty()) {
minStack.pop();
}
/* 初始化最小栈的栈顶元素为最大值为了防止top访问空指针报错 */
minStack.push(INT_MAX);
}
void push(int x) {
st1.push(x);
if(st2.empty())//如果辅助栈为空,就直接入栈:
st2.push(x);
else//如果辅助栈非空,入栈的时候再做比较,把小的值入栈:
st2.push(x > st2.top() ? st2.top() : x);
}
void pop() {
st1.pop();
st2.pop();
}
int top() {
return st1.top();
}
int min() {
return st2.top();
}
};
/**
* Your MinStack object will be instantiated and called as such:
* MinStack* obj = new MinStack();
* obj->push(x);
* obj->pop();
* int param_3 = obj->top();
* int param_4 = obj->min();
*/
面试题31:栈的压入、弹出序列
看了答案的代码之后,整理了下思路:
步骤一:把压栈序列的元素入栈到辅助栈中;
步骤二:然后判断栈顶元素是否等于弹出序列的元素:
如果等于并且辅助栈非空,就出栈,继续遍历弹出序列的下一个元素;
如果不等于就重复步骤一和步骤二,直到压栈序列的元素全部入栈;
步骤三:判断辅助栈是否为空:
如果为空就说明弹出序列和压栈序列是对应的,返回true
;
如果非空就说明不对应,返回false
。
acwing上的思路:(表达的更简洁)
用一个新栈s
来模拟实时进出栈操作:
在for循环中里依次喂数,每push
一个数字就检查有没有能pop
出来的;
如果最后s
为空,说明一进一出刚刚好。
时间复杂度分析:一共push n次,pop n次。
代码如下:
class Solution {
public:
bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
//如果两个序列长度不相同,必不匹配:
if(pushed.size() != popped.size()) return false;
//辅助栈:
stack<int> st;
//弹出序列的下标:
int j = 0;
//遍历压栈序列,并入栈到辅助栈中:
for(int i = 0; i < pushed.size(); i++){
st.push(pushed[i]);
//栈非空 且 辅助栈的栈顶元素等于弹出序列的元素:
while(!st.empty() && st.top() == popped[j]){
//就出栈:
st.pop();
//遍历下一个弹出序列的元素:
++j;
}
}
if(st.empty())
return true;
else
return false;
//或者可以直接写return st.empty();
}
};
书上的思路:
判断一个序列是不是栈的弹出序列:
- 如果下一个弹出的数字刚好是栈顶数字,那么直接弹出;
- 如果下一个弹出的数字不在栈顶,则把压栈序列中还没有入站的数字压入辅助栈,直到把下一个需要弹出的数字压入栈顶为止;
- 如果所有数字都压入栈后仍然没有找到下一个弹出的数字,那么该序列不可能是一个弹出序列。
照着上面的思路写的代码(自己写的,跟答案还不太一样):
class Solution {
public:
bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
//辅助栈:
stack<int> st1;
//给辅助栈中先存一个数,否则刚开始当栈为空时,判断栈顶元素是否等于tmp会出错
st1.push(-1);
//压栈序列的下标:
int index = 0;
//遍历弹出序列:
for(int i = 0; i < popped.size(); i++){
int tmp = popped[i];//4 5 3
int j;
if(st1.top() == tmp){
st1.pop();
continue;
}
for(j = index; j < pushed.size() && pushed[j] != tmp; j++){
st1.push(pushed[j]);
}
if(j == pushed.size())
return false;
if(pushed[j] == tmp){
//找到了就不用入栈了,直接把下标j自加一
//st1.push(pushed[j]);
index = ++j;
}
}
return true;
}
};
面试题32:不分行从上到下打印二叉树
面试题32’:分行从上到下打印二叉树
面试题32’':之字形打印二叉树
前两个题的区别就在于返回值是一维数组还是二维数组,
面试题32
返回一个vector<int> res;
;
面试题32'
返回一个vector<vector<int>> res;
,这道题和力扣上的第102题:二叉树的层序遍历相同;
面试题32''
是面试题32'
(层序遍历)的一个变形:第一层从左到右,第二层从右到左,…依次类推。这道题和力扣上的力扣103. 二叉树的锯齿形层序遍历相同。
代码:(面试题32
)
class Solution {
public:
vector<int> printFromTopToBottom(TreeNode* root) {
vector<int> res;
if(root == NULL) return res;
//层次遍历:
queue<TreeNode*> q;
q.push(root);
while(!q.empty()){
TreeNode* tmp = q.front();
res.push_back(tmp->val);
q.pop();
if(tmp->left) q.push(tmp->left);
if(tmp->right) q.push(tmp->right);
}
return res;
}
};
代码:(面试题32'
)
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
if(root == nullptr) return res;
queue<TreeNode*> q;
q.push(root);
while(!q.empty()){
vector<int> v;
int num = q.size();
for(int i = 0; i < num; i++){
TreeNode* tmp = q.front();
v.push_back(tmp->val);
q.pop();
if(tmp->left) q.push(tmp->left);
if(tmp->right) q.push(tmp->right);
}
res.push_back(v);
}
return res;
}
};
代码:(面试题32''
)
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
if(root == NULL) return res;
queue<TreeNode*> q;
q.push(root);
int count = 0;
while(!q.empty()){
++count;
int num = q.size();
vector<int> v;
for(int i = 0; i < num; i++){
TreeNode* tmp = q.front();
v.push_back(tmp->val);
q.pop();
if(tmp->left) q.push(tmp->left);
if(tmp->right) q.push(tmp->right);
}
//偶数层翻转v的内容:
if(count % 2 == 0) reverse(v.begin(), v.end());
res.push_back(v);
}
return res;
}
};
面试题33:二叉搜索树的后序遍历序列
这题跟面试题7:重建二叉树(利用前序遍历和中序遍历重建一颗二叉树)有点类似,
二叉搜索树的后序遍历:
- 后序遍历:左子树->右子树->根;
- 且左子树的值都小于根,右子树的值都大于根;
- 二叉搜索树的中序遍历会得到一个递增的序列,逆中序遍历会得到一个递减的序列;
- 所以对二叉搜索树的后序遍历数组进行排序,会得到此二叉搜索树的中序遍历结果;
- 这样就可以通过中序遍历和后序遍历来重建这颗二叉树。
但是这道题重建二叉树没啥意义,题目考查的时候判断所给的数组是不是某二叉搜索树的后序遍历结果,所以可以说是一道数组题。
思路:
说到底就是用递归遍历这个数组,
递归停止的条件:左右边界相遇,此时这个子树肯定是对的,返回true;
递归的内容:
序列的最右边是根结点的值root,通过它可以找到左右子树的边界mid,遍历数组,直到遇到某个值大于root,这个值就是右子树的第一个节点,然后再判断右子树中有没有值是小于root的(右子树的值应该都大于root才对),如果有就返回false,如果没有,就递归进入下一层,只有当左右子树都满足条件才能返回true,表示这个序列是一个二叉搜索树的后续遍历序列。
代码:
class Solution {
public:
bool verifyPostorder(vector<int>& postorder) {
int num = postorder.size();
bool res = recur(0, num - 1, postorder);
return res;
}
bool recur(int l, int r, vector<int>& postorder){
//当数组的长度为1时,返回true
if(l >= r) return true;
//递归的操作:
int root = postorder[r];//根节点的值
//先找到左右子树的边界mid
// int mid;
// for(int i = l; i < r; i++){
// if(postorder[i] > root){
// mid = i;
// break;
// }
// }
//先找到左右子树的边界mid
int mid = l;
for(; mid < r; mid++){
if(postorder[mid] > root){
break;
}
}
//然后判断右子树中有没有比根节点小的:
for(int i = mid; i < r; i++){
if(postorder[i] < root)
return false;
}
//进入下一层递归:
return recur(l, mid - 1, postorder) && recur(mid, r - 1, postorder);
}
};
书上的内容:
扩展:
输入一个整数数组,判断该数组是不是某二叉搜索树的前序遍历结果。
思路:
这个上面的问题很类似,只不过数组的顺序变成了根->左子树->右子树
,核心代码如下:
int root = postorder[l];//根节点的值
//先找到左右子树的边界mid
int mid = l + 1;
for(; mid <= r; mid++){
if(postorder[mid] > root){
break;
}
}
//然后判断右子树中有没有比根节点小的:
for(int i = mid; i <= r; i++){
if(postorder[i] < root)
return false;
}
//进入下一层递归:
return recur(l + 1, mid - 1, postorder) && recur(mid, r, postorder);
举一反三:
如果面试题要求处理一棵二叉树的遍历序列,则可以先找到二叉树的根节点,再基于根节点把整棵树的遍历序列拆分成左子树对应的子序列和右子树对应的子序列,接下来再递归地处理这两个子序列。本面试题应用的是这种思路,面试题7:重建二叉树应用的也是这种思路。
面试题34:二叉树中和为某一值的路径 --> 面试题27:二叉树的镜像
给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
解题思路:
本问题是典型的二叉树方案搜索问题,使用回溯法解决,其包含 先序遍历 + 路径记录 两部分。
- 先序遍历: 按照 “根、左、右” 的顺序,遍历树的所有节点。
- 路径记录: 在先序遍历中,记录从根节点到当前节点的路径。
当路径为 ① 根节点到叶节点形成的路径且
② 各节点值的和等于目标值sum
时 ,将此路径加入结果列表。
算法流程:
pathSum(root, sum) 函数:
- 初始化: 结果列表 res ,路径列表 path 。
- 返回值: 返回 res 即可。
recur(root, tar) 函数:
- 递推参数: 当前节点 root ,当前目标值 tar 。
- 终止条件: 若节点 root 为空,则直接返回。
- 递推工作:
路径更新: 将当前节点值 root.val 加入路径 path ;
目标值更新: tar = tar - root.val(即目标值 tar 从 sum 减至 00 );
路径记录: 当 ① root 为叶节点 且 ② 路径和等于目标值 ,则将此路径 path 加入 res 。
先序遍历: 递归左 / 右子节点。
路径恢复: 向上回溯前,需要将当前节点从路径 path 中删除,即执行 path.pop() 。
答案中是用target - root->val
,直到target为0,我用sum += root->val
来做,回溯的时候除了把pash的最后一个元素剔除掉,sum也要减去这个要剔除的值。
代码:(自己能理解的和答案)
//自己觉得比较好理解的写法:
class Solution {
public:
vector<vector<int>> pathSum(TreeNode* root, int target) {
if(root == nullptr) return res;
dfs(root, target);
return res;
}
private:
vector<vector<int>> res;//存储最终的结果
vector<int> path;//存储单条路径
int sum = 0;//记录单条路径的和
void dfs(TreeNode* root, int target){
//递归停止的条件:遇到叶节点
if(root == nullptr) return;
//root非空,就把它的值放到path数组中:
path.push_back(root->val);
sum += root->val;
//然后判断root是不是叶节点并且此时的和是不是target:
if(!root->left && !root->right && sum == target)
res.push_back(path);
//如果不满足条件(不是叶结点或者sum不等于target),就继续递归遍历root的左右子树:
dfs(root->left, target);// - root->val
dfs(root->right, target);// - root->val
//如果遍历到叶节点了sum还不等于target,就把最近的一个叶结点剔除,去另外一条路径
sum -= root->val;// path.back();都可以
path.pop_back();
}
};
//答案:
class Solution {
public:
vector<vector<int>> pathSum(TreeNode* root, int target) {
recur(root, target);
return res;
}
private:
vector<vector<int>> res;//存储最终的结果
vector<int> path;//存储单条路径
void recur(TreeNode *root, int tar) {
if (root == nullptr) return;
path.push_back(root->val);
tar -= root->val;
if (tar == 0 && root->left == nullptr && root->right == nullptr) {
res.push_back(path);
}
recur(root->left, tar);
recur(root->right, tar);
path.pop_back();
}
};
这个题和二叉树的遍历以及面试题27:二叉树的镜像有点像,都是遍历整个二叉树,只不过递归的内容不一样:
二叉树的遍历:
if(root == nullptr) return;
//如果不是空,就把root的val值存到vector容器中,然后通过递归进入到root的左右子树,直到遍历到root为空,即遇到叶子结点。
res.push_back(root->val);
recur(root->left, res);
recur(root->right, res);
二叉树的镜像:
if(node == NULL) return;
//如果不是空,就把root的左右孩子进行交换,然后通过递归进入到root的左右子树,直到遍历到root为空,即遇到叶子结点。
TreeNode* tmp = node->left;
node->left = node->right;
node->right = tmp;
recur(node->left);
recur(node->right);
二叉树的路径和等于target:
//递归停止的条件:遇到叶节点
if(root == nullptr) return;
//root非空,就把它的值放到path数组中:
path.push_back(root->val);
sum += root->val;
//然后判断root是不是叶节点并且此时的和是不是target:
if(!root->left && !root->right && sum == target)
res.push_back(path);
//如果不满足条件,就递归遍历root的左右子树:
dfs(root->left, target);
dfs(root->right, target);
//如果遍历完左右子树sum还不等于target,就把最近的一个叶结点剔除,去另外一条路径
sum -= root->val;// path.back();都可以
path.pop_back();
4.4 分解让复杂问题简单化
面试题35:复杂链表的复制
首先想到的常规方法:
class Solution {
public:
Node* copyRandomList(Node* head) {
if(head == NULL) return NULL;
Node* cur = head;//一般不直接用head
Node* preHead = new Node(0);
Node* p = preHead;
while(head){
Node* tmp = new Node(cur->val);
//tmp->random = cur->random; 这里不确定,所以这种写法不对
p->next = tmp;
p = p->next;
cur = cur->next;
}
return preHead->next;
}
};
由于tmp->random = cur->random;
这里不确定,所以这种写法不对。
答案中提供了两种方法:哈希表 和 拼接+拆分。
哈希表方法:(时间复杂度:O(N
),空间复杂度:O(N)
)
class Solution {
public:
Node* copyRandomList(Node* head) {
if(head == NULL) return NULL;
Node* cur = head;
//创建哈希表:
unordered_map<Node*, Node*> map;
//复制原链表的各个节点,并建立“原节点->新节点”的映射:
while(cur){
map[cur] = new Node(cur->val);
cur = cur->next;
}
//构建新链表的next和random指向:
cur = head;//这个别忘了!!!
while(cur){
map[cur]->next = map[cur->next];//这里不是cur->next
map[cur]->random = map[cur->random];//这里不是cur->random
cur = cur->next;
}
//返回新链表的头节点:
return map[head];
}
};
拼接+拆分方法:(时间复杂度:O(N
),空间复杂度:O(1)
)
class Solution {
public:
Node* copyRandomList(Node* head) {
if(head == NULL) return NULL;
Node* cur = head;
//拼接表:
while(cur){
Node* tmp = cur->next;
Node* copyNode = new Node(cur->val);
cur->next = copyNode;
copyNode->next = tmp;
cur = tmp;
}
//弄好random指向:(next指向在拆分表的时候弄)
cur = head;
while(cur){
if(cur->random != NULL)
cur->next->random = cur->random->next;
cur = cur->next->next;//一次跳两个
}
//拆分表:
Node* preHead = new Node(0);
Node* p = preHead;
cur = head;
while(cur){
//新链表:
p->next = cur->next;
p = p->next;
//原链表:
cur->next = cur->next->next;
cur = cur->next;
}
return preHead->next;
}
};
面试题36:二叉搜索树与双向(循环)链表
力扣上的这道题直接是把二叉搜索树转换成一个双向循环链表。
解题思路:
dfs(cur)
: 递归法中序遍历;
- 终止条件: 当节点
cur
为空,代表越过叶节点,直接返回; - 递归左子树,即 dfs(cur.left) ;
- 构建链表:
当 pre 为空时: 代表正在访问链表头节点,记为head
;
当 pre 不为空时: 修改双向节点引用,即 pre.right = cur , cur.left = pre ;
保存 cur : 更新 pre = cur ,即节点 cur 是后继节点的 pre ; - 递归右子树,即 dfs(cur.right) ;
treeToDoublyList(root)
:二叉树to双向循环链表
- 特例处理: 若节点 root 为空,则直接返回;
- 初始化: 空节点 pre ;
- 转化为双向链表: 调用 dfs(root) ;
- 构建循环链表: 中序遍历完成后,
head
指向头节点,pre
指向尾节点,因此修改 head 和 pre 的双向节点引用即可; - 返回值: 返回链表的头节点
head
即可;
class Solution {
public:
Node* treeToDoublyList(Node* root) {
if(root == nullptr) return nullptr;
//转化为双向链表:
dfs(root);
//构建循环链表:进行头节点和尾节点的相互指向,这两句的顺序也是可以颠倒的
head->left = pre;
pre->right = head;
return head;
}
private:
Node *pre, *head;//初始化为空
void dfs(Node* cur) {
if(cur == nullptr) return;
dfs(cur->left);
//pre用于记录双向链表中位于cur左侧的节点,即上一次迭代中的cur,pre!=null时,cur左侧存在节点pre,需要进行pre.right=cur的操作。
if(pre != nullptr)
pre->right = cur;
//反之,当pre==null时,cur左侧没有节点,即此时cur为双向链表中的头节点
else
head = cur;
//pre是否为null对这句没有影响,且这句放在上面两句if else之前也是可以的。
cur->left = pre;
pre = cur;
dfs(cur->right);//全部迭代完成后,pre指向双向链表中的尾节点
}
};
自己又写了一遍:
class Solution {
public:
TreeNode* convert(TreeNode* root) {
if(root == NULL) return NULL;
dfs(root);
//如果要转换成双向循环链表,就把下面两行加上;如果只是转换成双向链表,就不用写下面的两行
head->left = pre;
pre->right = head;
return head;
}
private:
TreeNode* pre;//前一个结点
TreeNode* head;//头结点
void dfs(TreeNode* cur){
//递归返回的条件:
if(cur == NULL) return;
//遍历左子树:
dfs(cur->left);
//维护当前节点和它前面的节点的关系:
//前一个结点非空,它后面的节点就是当前节点;前一个节点为空,那么当前节点就是最终的双向链表的头结点
if(pre != NULL)
pre->right = cur;
else
head = cur;
//当前节点的左就是它前面的结点:
cur->left = pre;
//更新前一个节点的位置:即当前节点 cur 是后继节点的 pre
pre = cur;
//遍历右子树:
dfs(cur->right);
}
};
cur
写成root
:
class Solution {
public:
Node* treeToDoublyList(Node* root) {
if(root == NULL) return NULL;
//二叉搜索树转换成双向链表:
dfs(root);
//转换成循环链表:
head->left = pre;
pre->right = head;
return head;
}
private:
Node* head;
Node* pre;
void dfs(Node* root){
if(root == NULL) return;
dfs(root->left);
if(pre == NULL)
head = root;
else
pre->right = root;
root->left = pre;
pre = root;
dfs(root->right);
}
};
面试题37:序列化二叉树
难度:(困难)
先不做了
面试题38:字符串的排列
相关题目:
面试题17:打印从1到最大的n位数(用字符串表示一个大数)–>全排列+递归
面试题34:二叉树中和为某一值的路径 -->回溯(每个元素只能出现一次)
题目:
输入一个字符串,打印出该字符串中字符的所有排列;
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
示例1:
- 输入:s = “abc”
输出:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]
示例2:
- 输入:s = “abb”
输出:[“abb”,“bab”,“bba”]
示例 | 全排列 | 全排列 + 去重(用set容器实现) | 全排列 + 回溯(每个元素只能出现一次) | 全排列 + 去重 + 回溯(每个元素只能出现一次) |
---|---|---|---|---|
s = “abc” | 27种 | 27种 | 6种 | 6种 |
abc, acb, bac, bca, cab, cba, | abc, acb, bac, bca, cab, cba, | |||
s = “abb” | 27种 | 8种 | 6种 | 3种 |
aaa, aab, aba, abb, baa, bab, bba, bbb, | abb, abb, bab, bba, bab, bba, | abb, bab, bba, |
画图:
s = “abc”;
s = “abb”;
思路:
全排列 —> 回溯 —> 去重
去重:
- 用
set
来存储结果,每遇到一个可能的答案,就直接insert
进去。由于set不允许容器中有重复的元素,所有元素都会在插入时自动被排序,默认是升序,因此可以达到我们的目标。
回溯:
- 首先设置
vector<bool> visited(s.size(), false);
用vector<bool>
来记录每个下标的字符是否被使用过,即每个元素只能出现一次; - 然后
if(visited[i])
continue;//如果已经访问过了就进入下一个字符
str[index] = s[i];
visited[i] = true;
dfs(s, index + 1, visited);
//回溯:
visited[i] = false;
全排列:
- 首先
str.resize(s.size());
- 然后递归
dfs(s, 0);
递归三要素:
递归停止条件index == s.size()
,然后把str给到set
递归的操作:给str的每一位都赋上s中的每个字符
for(int i = 0; i < s.size(); i++){
if(visited[i])
continue;//如果已经访问过了就进入下一个字符
str[index] = s[i];
visited[i] = true;
dfs(s, index + 1, visited);
//回溯:
visited[i] = false;
}
代码:
class Solution {
public:
vector<string> permutation(string s) {
vector<bool> visited(s.size(), false);
str.resize(s.size());//
dfs(s, 0, visited);
for(auto i = store.begin(); i != store.end(); i++){
//cout << *i << endl;
res.push_back(*i);
}
//for(auto ele : store) res.push_back(ele);
return res;
}
private:
vector<string> res;
set<string> store;
string str = "";
void dfs(string& s, int index, vector<bool>& visited){
if(index == s.size()){
store.insert(str);
return;
}
for(int i = 0; i < s.size(); i++){
if(visited[i])
continue;//如果已经访问过了就进入下一个字符
str[index] = s[i];
//str += s[i];//str.push_back(s[i]);//这个改成上面一行
visited[i] = true;
dfs(s, index + 1, visited);
//回溯:和面试题34很像
visited[i] = false;
//str.pop_back();//这个不要
}
}
};
acwing上的题目是51. 数字排列:输入一组数字(可能包含重复数字),输出其所有的排列方式。
思路一样,把string换成vector<int> nums
而已,也是用全排列+回溯+去重的方法来实现,代码如下:
class Solution {
public:
vector<vector<int>> permutation(vector<int>& nums) {
vector<bool> visited(nums.size(), false);
path.resize(nums.size());
dfs(nums, 0, visited);
for(auto i = store.begin(); i != store.end(); i++)
res.push_back(*i);
return res;
}
private:
vector<vector<int>> res;
set<vector<int>> store;
vector<int> path;
void dfs(vector<int>& nums, int index, vector<bool>& visited){
if(index == nums.size()){
store.insert(path);
//res.push_back(path);
return;
}
for(int i = 0; i < nums.size(); i++){
if(visited[i])
continue;
path[index] = nums[i];
visited[i] = true;
dfs(nums, index + 1, visited);
visited[i] = false;
}
}
};
补充leetcode上的一个答案:(和面试题34有点像)
class Solution {
public:
vector<string> permutation(string s) {
vector<bool> visited(s.size(), false);
//str.resize(s.size());//没有这行
dfs(s, visited);
for(auto i = store.begin(); i != store.end(); i++){
//cout << *i << endl;
res.push_back(*i);
}
//for(auto ele : store) res.push_back(ele);
return res;
}
private:
vector<string> res;
set<string> store;
string str = "";
void dfs(string& s, vector<bool>& visited){
if(str.size() == s.size()){
store.insert(str);
return;
}
for(int i = 0; i < s.size(); i++){
if(visited[i]) continue;//如果已经访问过了就进入下一个字符
str += s[i];//str.push_back(s[i]);//
visited[i] = true;
dfs(s, visited);
//回溯:和面试题34很像
visited[i] = false;
str.pop_back();
}
}
};
再来看看书上的方法,和力扣上的剑指 Offer 38. 字符串的排列(回溯法,清晰图解)类似,还是看不懂交换是啥意思。