2023 秋招面经

2023 秋招资料

算法数据结构

螺旋矩阵问题

螺旋矩阵I(将 1~n*m 个数按蛇形方向填入数组中)

记录蛇形矩阵偏移量方法,四个不同方向右下左上,判断什么情况下矩阵遍历完,1.要么出界 2.要么走完

 class Solution {
 public:
     vector<int> spiralOrder(vector<vector<int>>& matrix) {
         int n = matrix.size(), m = matrix[0].size();
         int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0};
         vector<int> res;
         vector<vector<bool>> vis(n, vector<bool>(m));
         for (int i = 0, x = 0, y = 0, d = 0; i < n * m; i ++) {
             res.push_back(matrix[x][y]);
             vis[x][y] = true;
             int nx = x + dir[d][0], ny = y + dir[d][1];
             if (nx < 0 || nx >= n || ny < 0 || ny >= m || vis[nx][ny]) {
                 d = (d + 1) % 4;
                 nx = x + dir[d][0], ny = y + dir[d][1];
             }
             x = nx, y = ny;
         }
 
         return res;
     }
 };

螺旋矩阵 II

desc: 给定 n 生成一个包含一到 n^2的矩阵, 按照螺旋矩阵的方式存储

解法:和螺旋矩阵 I 类似,通过偏移量构造

 class Solution {
 public:
     vector<vector<int>> generateMatrix(int n) {
         vector<vector<int>> res(n, vector<int>(n, 0));
         int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0};
        for (int x = 0, y = 0, cnt = 1, d = 0; cnt <= n * n; cnt ++) {
            res[x][y] = cnt;
            int nx = x + dir[d][0], ny = y + dir[d][1];
            if (nx < 0 || nx >= n || ny < 0 || ny >= n || res[nx][ny]) {
                d = (d + 1) % 4;
                nx = x + dir[d][0], ny = y + dir[d][1];
            }
            x = nx, y = ny;
        } 
         return res;
     }
 };

螺旋矩阵 III

desc:给定一个nxm的矩阵,和一个开始走的起点坐标,求螺旋方向走的坐标,并且可以出界, 如下图所示

 class Solution {
 public:
     vector<vector<int>> spiralMatrixIII(int n, int m, int x, int y) {
         vector<vector<int>> res;
         int tot = n * m;
         res.push_back({x, y});
         int d = 0;
         int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0};
         // k 循环表示模拟圆环的半径, 有一个规律1, 1, 2, 2, 3, 3...
         for (int k = 1; res.size() < tot; k ++) {
             // i 循环表示矩阵是按照每个半径会走两次
             for (int i = 0; i < 2 && res.size() < tot; i ++) {
                 // j循环表示开始走半径为 k 的元素
                 for (int j = 0; j < k && res.size() < tot; j ++) {
                         int nx = x + dir[d][0], ny = y + dir[d][1];
                         // 在矩阵内即可添加进去
                         if (nx >= 0 && nx < n && ny >= 0 && ny < m)
                             res.push_back({nx, ny});
                         x = nx, y = ny;
                 }
                 d = (d + 1) % 4;
             }
         }
         return res;
     }
 };
 

螺旋矩阵 IV

desc: 给定链表从链表中构建一个蛇形矩阵,和 I、II 的构建方式一样

 class Solution {
 public:
     vector<vector<int>> spiralMatrix(int n, int m, ListNode* head) {
         vector<vector<int>> res(n, vector<int>(m, -1));
         int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0};
         int x = 0, y = 0, d = 0;
         for (auto p = head; p != nullptr; p=p->next) {
             res[x][y] = p->val;
             int nx = x + dir[d][0], ny = y + dir[d][1];
             if (nx < 0 || nx >= n || ny < 0 || ny >= m || res[nx][ny] != -1) {
                 d = (d + 1) % 4;
                 nx = x + dir[d][0], ny = y + dir[d][1];
             }
             x = nx, y = ny;
         }
         return res;
     }
 };

单链表排序问题

单链表快速排序(时间O(nlogn), 空间(O(logn)))

![image-20220831102437989](file:///Users/liyan/Library/Application%20Support/typora-user-images/image-20220831102437989.png?lastModify=1662036810)

链表快排基本思路

  • 选定链表头基准元素
  • 建立三个子链表left,mid,right
    • left存储比基准元素小的节点
    • mid 存储和基准元素相等的节点
    • right 存储比基准元素大的节点
  • 递归的去排序第二个过程
    • 递归出口节点空或者链表只有一个节点
  • 最后将 left 拼接 mid 拼接 right
 /**
  * Definition for singly-linked list.
  * struct ListNode {
  *     int val;
  *     ListNode *next;
  *     ListNode() : val(0), next(nullptr) {}
  *     ListNode(int x) : val(x), next(nullptr) {}
  *     ListNode(int x, ListNode *next) : val(x), next(next) {}
  * };
  */
 class Solution {
 public:
     ListNode* getTail(ListNode* head) {
         while (head->next) head=head->next;
         return head;
     }
     ListNode* sortList(ListNode* head) {
         if (!head || !head->next) return head;
 
         auto left = new ListNode(-1), mid = new ListNode(-1), right = new ListNode(-1);
         auto ltail = left, rtail = right, mtail = mid;
         // int cnt = 0;
         // int val = head->val;
         // for (auto p = head; p; p = p->next) {
         //     cnt ++;
         // }
         // int n = cnt;
         // for (auto p = head; p; p = p->next) {
         //     cnt --;
         //     if (cnt == n / 2) {
         //         val = p->val;
         //         break;
         //     }
             
         // }
         int val = head->val;
         for (auto p = head; p; p=p->next) {
             if (p->val < val) ltail = ltail->next = p;
             else if (p->val == val) mtail = mtail->next = p;
             else rtail = rtail->next = p;
         }
         ltail->next = mtail->next = rtail->next = nullptr;
         left->next = sortList(left->next);
         right->next = sortList(right->next);
 
         getTail(left)->next = mid->next;
         getTail(left)->next = right->next;
         auto p = left->next;
         delete left;
         delete mid;
         delete right;
         return p;
     }
 };

时间复杂度O(nlogn)、空间复杂度O(logn), 疑问:为什么过不了leetcode 原题

可以将基准值随机选取而不是选取头尾节点

单链表排序(归并)(时间:O(nlogn) 空间:常数,迭代)

迭代写法

想法:

  • 先求出要排序的链表结点数 n
  • i 循环控制排序每一个子区间,子区间长度为 1,2,4,8...2^n
  • 定义一个 dummy 节点为初始节点
  • j循环控制合并每个子区间, j + i <= n, j += 2 * i;
  • p 指向合并子区间第一个节点,q 指向合并子区间第二个节点, 通过 while 循环控制
 /**
  * Definition for singly-linked list.
  * struct ListNode {
  *     int val;
  *     ListNode *next;
  *     ListNode() : val(0), next(nullptr) {}
  *     ListNode(int x) : val(x), next(nullptr) {}
  *     ListNode(int x, ListNode *next) : val(x), next(next) {}
  * };
  */
 class Solution {
 public:
     ListNode* sortList(ListNode* head) {
         int n = 0; // 记录链表节点个数
         for (auto p = head; p; p = p->next) n ++; 
         auto dummy = new ListNode(-1); // 虚拟头结点,指向排序好的第一个节点
         dummy->next = head; 
 
         for (int i = 1; i < n; i *= 2) { // 循环 log 层, 小于n是因为等于n时说明所有元素均归并完毕,大于n时同理
             auto cur = dummy; // cur表示准备排序子区间长度为 i 的起始位置
             for (int j = 1; j + i <= n; j += 2 * i) { // j代表每一段的开始,每次将两段有序段归并为一个大的有序段,故而每次+2i
                 auto p = cur->next, q = p; // p 是第一段起始位置, q是第二段起始位置
                 for (int k = 0; k < i; k ++) q = q->next; 
                 int l = 0, r = 0; 
                 while (l < i && r < i && p && q) {
                     if (p->val <= q->val) cur = cur->next=p, p = p->next, l ++;
                     else cur = cur->next=q, q = q->next, r ++;
                 }
                 while (l < i && p) cur = cur->next = p, p = p->next, l ++;
                 while (r < i && q) cur = cur->next = q, q = q->next, r ++;
                 cur->next = q;
             }
         }
         return dummy->next;
     }
 };
递归写法O(nlogn) + 空间O(logn)

通过递归实现链表归并排序,有以下两个环节:

  • 分割 cut 环节: 找到当前链表中点,并从中点将链表断开(以便在下次递归 cut 时,链表片段拥有正确边界);

    • 我们使用 fast,slow 快慢双指针法,奇数个节点找到中点,偶数个节点找到中心左边的节点。
    • 找到中点 slow 后,执行 slow.next = None 将链表切断。
    • 递归分割时,输入当前链表左端点 head 和中心节点 slow 的下一个节点 tmp(因为链表是从 slow 切断的)。
    • cut 递归终止条件: 当head.next == None时,说明只有一个节点了,直接返回此节点。
  • 合并 merge 环节: 将两个排序链表合并,转化为一个排序链表。

    • 双指针法合并,建立辅助ListNode h 作为头部。
    • 设置两指针 left, right 分别指向两链表头部,比较两指针处节点值大小,由小到大加入合并链表头部,指针交替前进,直至添加完两个链表。
    • 返回辅助ListNode h 作为头部的下个节点 h.next。
    • 时间复杂度 O(l + r),l, r 分别代表两个链表长度。
     ListNode* merge(ListNode* l, ListNode* r) {
             ListNode *dummy = new ListNode(-1);
             auto cur = dummy;
             while (l && r) {
                 if (l->val <= r->val) cur = cur->next = l, l = l->next;
                 else cur = cur->next = r, r=r->next;
             }
             cur->next = (l ? l : r);
             return dummy->next;
         }
         ListNode* sortList(ListNode* head) {
             if (!head || !head->next) return head;
     
             ListNode* fast = head, *slow = head; // 快慢指针找到中间的 cut 节点分为左右两段
             while (fast->next && fast->next->next) {
                 fast = fast->next->next;
                 slow = slow->next;
             }
             fast = slow;
             slow = slow->next; // cut 的右边的首个节点
             fast->next = nullptr; // fast 是 cut 后的左边的最后一个节点
     
             ListNode* l = sortList(head), *r = sortList(slow);
             return merge(l, r); // 合并
         }
    

单链表排序插入

插入排序 算法的步骤:

  1. 插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。
  2. 每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。
  3. 重复直到所有输入数据插入完为止。

为了方便处理边界情况,我们建立虚拟头结点,指向原链表头部。
然后扫描原链表,对于每个节点 vv,从前往后扫描结果链表,找到第一个比 vv 大的节点 uu,将 vv 插入到 uu 之前。

时间复杂度分析:一共遍历 nn 个节点,对于每个节点找合适位置时,最多需要遍历 O(n)O(n) 次,所以总时间复杂度是 O(n2)O(n2)。

 /**
  * Definition for singly-linked list.
  * struct ListNode {
  *     int val;
  *     ListNode *next;
  *     ListNode() : val(0), next(nullptr) {}
  *     ListNode(int x) : val(x), next(nullptr) {}
  *     ListNode(int x, ListNode *next) : val(x), next(next) {}
  * };
  */
 class Solution {
 public:
     ListNode* insertionSortList(ListNode* head) {
         ListNode* dummy = new ListNode(-1);
         while (head) { // head 指向当前待插入的节点
             ListNode* next = head->next;   // next指向下一个循环待插入的节点
             ListNode* p = dummy; // dummy存的是待插入的链表
             while (p->next && p->next->val <= head->val) p = p->next; //找到插入 head 的位置 p
 
             head->next = p->next; // 将head 插入 p后面
             p->next = head; // 
 
             head = next; // head更新为下一个循环要插入的节点
 
         }
         return dummy->next;
 
     }
 };

单链表排序选择

选择排序过程

单链表排序冒泡

单链表排序堆排序

C++语言特性

C++ 中什么时候把析构函数写为虚函数 ?

当一个类为基类,通常其析构函数被声明为虚函数

why ?

因为如果定义基类指针,根据赋值兼容性问题,基类指针可以指向动态生成子类对象的地址,此时子类对象已经被充当基类使用

而在 delete 基类对象时,如果不定义析构函数为虚函数,则不会调用派生类的析构函数,这样就造成内存泄漏了.

C++中可以把构造函数声明为虚函数 ?

不可以。因为虚函数的调用依靠于虚函数表。然而在构造函数执行完时,虚函数表指针才正确初始化

构造函数不可以声明为虚函数,那么可以实现多态 ?

不可以,因为析构函数一旦被调用,虚函数表指针就会被销毁。在构造函数中实现体重也不可能发生多态行为,因为在构造函数执行时,虚函数表指针还没被正确初始化。

计算机网络

Web Server 项目

数据库

操作系统

posted @ 2022-09-01 20:56  Lilyan&Code  阅读(51)  评论(0编辑  收藏  举报