手撕代码环节常常是面试官给出题目的口头或文字描述,要求在纸上手写或在txt文档中打字,面试以简单数据结构与算法题为主,考察基本代码功底。
考察频次:链表 > 字符串/哈希 > 二叉树与图 > 栈/队列 > 查找/排序/搜索 > 动态规划 > 计算机视觉 > 其他(数学/贪心/复杂数据结构)
链表:1~5
字符串/哈希:6~9
二叉树与图:10~13
栈/队列:14
查找/排序/搜索:15~17
动态规划:18~19
计算机视觉:20
1.链表判断是否有环(快手、美团、哈啰)
思路:快慢指针
链表是否存在环的问题是经典的快慢指针问题,不会的看这篇。 (如果一个链表存在环,fast走2,slow走1,那么快慢指针必然会相遇)
。如果将尾结点的 next 指针指向其他任意一个结点,那么链表就存在了一个环。快慢指针的特性 —— 每轮移动之后两者的距离会加一(通常是fast走2 slow走1;也可以fast走n slow走1;当然还可以先让fast走k步,再让slow和fast都每次走1,这种做法可以实现求倒数第k个链表元素)
。下面会继续用该特性解决环的问题。 当一个链表有环时,快慢指针都会陷入环中进行无限次移动,然后变成了追及问题。想象一下在操场跑步的场景,只要一直跑下去,快的总会追上慢的。当两个指针都进入环后,每轮移动使得慢指针到快指针的距离增加一,同时快指针到慢指针的距离也减少一,只要一直移动下去,快指针总会追上慢指针。
bool hasCycle(ListNode *head) {
ListNode* fast=head,* slow=head;
while(fast!=NULL){
fast = fast->next;
if(fast!=NULL) fast = fast->next; //如果fast没结束就再走第2步
else return false; //如果fast结束了就无循环
slow = slow->next;
if(slow==fast) return true; //如果fast追上slow就有循环
}
return false; //如果fast结束了就无循环
}
2. 链表中倒数第k个结点
思路1:快慢指针
,使用了一个虚拟头节点 dummy 来简化链表的操作。我们使用快慢指针的方法,让快指针先走n+1步(因为多走一步头节点dummy),然后快指针和慢指针同时往后遍历。当快指针到达链表尾部时,慢指针指向的是倒数第n个节点的前一个节点。然后我们删除倒数第n个节点(前一个节点的next = next->next),并重新连接链表。最后,我们返回新链表的头节点。(技巧:添加一个哑节点(dummy node),它的 next\textit{next}next 指针指向链表的头节点。这样一来,我们就不需要对头节点进行特殊的判断了。)
ListNode* removeNthFromEnd(ListNode* head, int n) {
if (head == nullptr) {
return nullptr;
}
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* fast = dummy;
ListNode* slow = dummy;
// fast先走n+1步
for (int i = 0; i <= n; i++) {
fast = fast->next;
}
while (fast != nullptr) {
fast = fast->next;
slow = slow->next;
}
// 现在slow指向倒数第n+1个节点的前一个节点
ListNode* temp = slow->next;
slow->next = temp->next;
delete temp;
ListNode* newHead = dummy->next; //重新获得第一个节点
delete dummy; //删除虚拟头节点
return newHead;
}
思路2:栈
,遍历链表的同时全部节点压栈,「先进后出」弹栈寻找倒数第n-1个节点/直接用vector模拟栈索引查找倒数第n-1个节点,将n-1的next=next->next执行删除操作。(特殊情况:当n=stk.size()时,要删除第1个节点,直接返回第二个节点即可)但时间和空间复杂度相比快慢指针要高。
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0,head);
vector<ListNode*> stk;
while(head!=nullptr){
stk.push_back(head);
head=head->next;
}
// 处理边界情况,当n等于链表长度时,删除第一个节点,直接返回第一个节点的下一个节点
if (n == stk.size()) {
ListNode* newHead = dummy->next->next;
delete dummy;
return newHead;
}
// 弹栈找到倒数第n-1个节点(弹n次后,栈顶即为n-1),这里直接查找
ListNode* node_n_pre = stk[stk.size()-n-1];
node_n_pre->next = node_n_pre->next->next;
ListNode* newHead = dummy->next;
delete dummy;
return newHead;
}
3. 反转链表/链表的某区间(猿辅导、美团)
3.1 反转链表
思路:栈反转链表
/递归
、双指针
栈反转链表
:链表的反转是老生常谈的一个问题了,同时也是面试中常考的一道题。最简单的一种方式就是使用栈,因为栈是先进后出的。实现原理就是把链表节点一个个入栈,当全部入栈完之后再一个个出栈,出栈的时候在把出栈的结点串成一个新的链表。
ListNode* reverseList(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return head; // 处理空链表或者只有一个节点的情况
}
stack<ListNode*> stk;
ListNode* curr = head;
while (curr != nullptr) {
//节点入栈
stk.push(curr);
curr = curr->next;
}
ListNode* newHead = stk.top();
stk.pop();
curr = newHead;
while (!stk.empty()) {
//节点出栈
curr->next = stk.top();
stk.pop();
curr = curr->next;
}
curr->next = nullptr; // 将链表结尾的next指针置为nullptr
return newHead; // 返回反转后的头节点
}
双指针
:考虑遍历链表,并在访问各节点时修改 next 引用指向,不断遍历旧链表的每个节点cur,将其指向新链表头new_head(new_head->next=nullptr
),其中为了能在赋值后找到cur的写一个节点,用临时节点 t 保存cur->next
。
ListNode* reverseList(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return head; // 处理空链表或者只有一个节点的情况
}
ListNode* new_head=nullptr;
ListNode* cur=head;
while(cur!=nullptr){
//new_head和cur交替遍历链表,同时修改指针方向
ListNode* t = cur->next;//临时节点保存cur->next
cur->next=new_head;
new_head = cur;
cur=t;
}
return new_head; // 返回反转后的头节点
}
递归
:
/*以链表1->2->3->4->5举例*/
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
/*
直到当前节点的下一个节点为空时返回当前节点
由于5没有下一个节点了,所以此处返回节点5
*/
return head;
}
//递归传入下一个节点,目的是为了到达最后一个节点
ListNode newHead = reverseList(head.next);
//一直传入下一个节点为head,所以每轮递归的head代表的节点都不一样
/*
第一轮出栈,head为5,head.next为空,返回5
第二轮出栈,head为4,head.next为5,执行head.next.next=head也就是5.next=4,
把当前节点的子节点的子节点指向当前节点
此时链表为1->2->3->4<->5,由于4与5互相指向,所以此处要断开4.next=null
此时链表为1->2->3->4<-5
返回节点5
第三轮出栈,head为3,head.next为4,执行head.next.next=head也就是4.next=3,
此时链表为1->2->3<->4<-5,由于3与4互相指向,所以此处要断开3.next=null
此时链表为1->2->3<-4<-5
返回节点5
第四轮出栈,head为2,head.next为3,执行head.next.next=head也就是3.next=2,
此时链表为1->2<->3<-4<-5,由于2与3互相指向,所以此处要断开2.next=null
此时链表为1->2<-3<-4<-5
返回节点5
第五轮出栈,head为1,head.next为2,执行head.next.next=head也就是2.next=1,
此时链表为1<->2<-3<-4<-5,由于1与2互相指向,所以此处要断开1.next=null
此时链表为1<-2<-3<-4<-5
返回节点5
出栈完成,最终头节点5->4->3->2->1
*/
head.next.next = head;
head.next = null;
return newHead; //一直返回尾节点
}
3.1 反转链表的某区间
思路:头插法
,在需要反转的区间里[left, right]
,每遍历到一个节点,让这个新节点来到反转部分的起始位置(pre之后)。下面的图展示了整个流程。
4. 合并两个有序链表
思路1:双指针
,选两个指针中最小的元素插入新链表尾,如果两着有一个先结束,直接把另一个链表剩余部分,接到新链表尾。
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if (l1 == nullptr) return l2;
if (l2 == nullptr) return l1;
ListNode* p1=l1,* p2=l2;
ListNode* new_tail,* new_head;//新链表头new_head、新链表尾new_tail
if(p1->val <= p2->val) {
new_tail=p1; p1=p1->next;}
else {
new_tail=p2; p2=p2->next;}
new_head=new_tail;
while(p1!=NULL && p2!=NULL){
//如果l1和l2都有元素,选最小的元素插入新链表
if(p1->val <= p2->val) {
new_tail->next=p1; p1=p1->next;}
else {
new_tail->next=p2; p2=p2->next;}
new_tail=new_tail->next;
}
if(p1!=NULL)//如果l2已经没元素,将l1后面的链表接在新链表尾上
new_tail->next=p1;
else if(p2!=NULL)//如果l1已经没元素,将l2后面的链表接在新链表尾上
new_tail->next=p2;
return new_head;
}
思路2:递归
,终止条件:当两个链表都为空时,表示我们对链表已合并完成。如何递归:我们判断 l1 和 l2 头结点哪个更小,然后较小结点的 next 指针指向其余结点的合并结果。(调用递归)(始终让当前两个链表中最小的节点,指向除该节点外的两个链表已经合并好的结果)
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
if(list1==nullptr)
return list2;
if(list2==nullptr)
return list1;
if(list1->val <= list2->val){
list1->next = mergeTwoLists(list1->next,list2);//小的链表头指向剩余两个链表的合并结果
return list1; //返回已经指向合并结果的当前小节点
} else{
list2->next = mergeTwoLists(list2->next,list1);//小的链表头指向剩余两个链表的合并结果
return list2; //返回已经指向合并结果的当前小节点
}
}
5. 链表排序(然后不能动指针)
6. 判断回文字符串
7. 最长回文子串
思路:贪心算法
8. 两个字符串的最大连续公共子串
dp 注意不是非连续
9. 最长不重复子串
leetcode(3)/剑指offer第二版(48)
10. 二叉树遍历
思路:递归
前序遍历(根左右)
:从二叉树的根结点出发,当第一次到达结点时就输出结点数据,按照先向左再向右的方向访问。对于上图,遍历顺序如下:ABDHIEJCFG
void preorder(Tnode* T){
if(T==NULL)return;
else{
cout<<T->data;
pre_travse(T->lchild);
pre_travse(T->rchild);
}
}
中序遍历(左根右)
:从二叉树的根结点出发,当第二次到达结点时就输出结点数据,按照先向左再向右的方向访问。对于上图,遍历顺序如下:HDIBJEAFCG
void midorder(Tnode* T){
if(T==NULL)return;
else{
post_travse(T->lchild);
cout<<T->data;
post_travse(T->rchild);
}
后序遍历(左右根)
:从二叉树的根结点出发,当第三次到达结点时就输出结点数据,按照先向左再向右的方向访问。对于上图,遍历顺序如下:HIDJEBFGCA
void postorder(Tnode* T){
if(T==NULL)return;
else{
post_travse(T->lchild);
post_travse(T->rchild);
cout<<T->data;
}
11. 二叉树最近公共祖先
思路:遍历
12. 二叉树深度及最长路径
13. 有序链表转换二叉搜索树(快手)
14. 两个栈实现队列(字节)
15. 二分查找(阿里巴巴)
16. 排序(快排、归并、堆排)
知道哪些排序算法,快排时间复杂度,时间复杂度推导,O(n)的排序方法
时间复杂度O(n)的排序算法
快排,归并,堆排序
17. DFS/BFS
18. 爬楼梯
剑指offer(八)
19. 扎气球/活动选择问题/会议室选择问题/时间安排问题
https://blog.csdn.net/yysave/article/details/84403875