手撕最常见的算法岗面试题(25道)

手撕代码环节常常是面试官给出题目的口头或文字描述,要求在纸上手写或在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_headnew_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

20. 实现IoU和NMS

猜你喜欢

转载自blog.csdn.net/weixin_54338498/article/details/132135545