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); // 合并 }
单链表排序插入
插入排序 算法的步骤:
- 插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。
- 每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。
- 重复直到所有输入数据插入完为止。
为了方便处理边界情况,我们建立虚拟头结点,指向原链表头部。
然后扫描原链表,对于每个节点 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++中可以把构造函数声明为虚函数 ?
不可以。因为虚函数的调用依靠于虚函数表。然而在构造函数执行完时,虚函数表指针才正确初始化
构造函数不可以声明为虚函数,那么可以实现多态 ?
不可以,因为析构函数一旦被调用,虚函数表指针就会被销毁。在构造函数中实现体重也不可能发生多态行为,因为在构造函数执行时,虚函数表指针还没被正确初始化。