写在前面
华为面试题库刷题第三次整理。
203. 移除链表元素
删除链表中等于给定值 val 的所有节点。
示例:
输入: 1->2->6->3->4->5->6, val = 6
输出: 1->2->3->4->5
解法:题目不难,重点是用多钟解法来解题。
递归
从尾部开始删除就不需要特别记录前一个节点,利用递归在这一点上就很讨巧。
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
if(!head) return NULL;//递归边界
head->next = removeElements(head->next,val);//递去:到底端再开始删除操作
return head->val==val?head->next:head;//递归式
}
};
迭代
遍历链表,删除节点。每次判断的都是下一位,这样方便删除,然后再最后return位置特判头。
代码:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
if(!head) return NULL;
ListNode *curr, * del;
curr = head;
while(curr->next){
if(curr->next->val==val){//删除下一位
del = curr->next;
curr->next = del->next;
delete del;
}
else{
curr = curr->next;
}
}
return head->val==val?head->next:head;//头元素特判
}
};
538. 把二叉搜索树转换为累加树
给定一个二叉搜索树(Binary Search Tree),把它转换成为累加树(Greater Tree),使得每个节点的值是原来的节点值加上所有大于它的节点值之和。
例如:
输入: 原始二叉搜索树:
5
/ \
2 13
输出: 转换为累加树:
18
/ \
20 13
解法:加上所有比这个节点大的,大的都在右边,所以从右往左遍历并累加就行了。
递归
代码:
/**
* 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 {
private:
int sum = 0;
public:
TreeNode* convertBST(TreeNode* root) {
if(root){
convertBST(root->right);//遍历右子树
sum += root->val;//累加
root->val = sum;//更新数值
convertBST(root->left);//遍历左子树
}
return root;
}
};
迭代
其实和递归大同小异,只不过把递归过程放进了循环里面。
代码:
class Solution {
public:
TreeNode* convertBST(TreeNode* root) {
int sum = 0;
TreeNode* t = root;
vector<TreeNode*> s;
while(!s.empty()||t){
while(t){
s.push_back(t);
t = t->right;
}
t = s.back();
s.pop_back();
sum += t->val;
t -> val = sum;
t = t -> left;
}
return root;
}
};
820. 单词的压缩编码
给定一个单词列表,我们将这个列表编码成一个索引字符串 S 与一个索引列表 A。
例如,如果这个列表是 [“time”, “me”, “bell”],我们就可以将其表示为 S = “time#bell#” 和 indexes = [0, 2, 5]。
对于每一个索引,我们可以通过从字符串 S 中索引的位置开始读取字符串,直到 “#” 结束,来恢复我们之前的单词列表。
那么成功对给定单词列表进行编码的最小字符串长度是多少呢?
提示:
- 1 <= words.length <= 2000
- 1 <= words[i].length <= 7
- 每个单词都是小写字母 。
解法:
后缀字符串
一个数组如果是另外一个字符串的后缀字符串,就会被删除,因为数据的单词长度不超过7,穷举即可,为了方便除重和删除操作,我们这里利用set数据结构。
代码:
class Solution {
public:
int minimumLengthEncoding(vector<string>& words) {
int l = words.size();
set<string> s(words.begin(),words.end());
for(auto word:words)
for(int i=1; i<word.length(); i++)
s.erase(word.substr(i));
int res = 0;
for(auto i = s.begin(); i != s.end(); i++)
res += (*i).length()+1;
return res;
}
};
字典树
上面那种方法明显不具有课扩展性,穷举单词的后缀字符串很繁琐,这题可以利用字典树来进行计算。
去找到是否不同的单词具有相同的后缀,我们将其反序之后放入一棵字典树中(前缀树)。例如,我们有 “time” 和 “me”,可以将 “emit” 和 “em” 放入字典树中。
然后,字典树的叶子节点(没有孩子的节点)就代表没有后缀的词,统计 sum(word.length + 1 for word in words)。
代码:
class Solution {
public:
struct TreeNode {
int len;
map<char, TreeNode*> next;
TreeNode() : len(0) {};
};
TreeNode* root;
Solution() {
root = new TreeNode();
}
int minimumLengthEncoding(vector<string>& words) {
int res = 0;
for (auto& w : words) {
auto node = root;
for (int i = w.size() - 1; i >= 0; --i) {
char c = w[i];
if (node->next.count(c) == 0) {
node->next[c] = new TreeNode();
}
node = node->next[c];
if (node->len > 0) {
res -= node->len;
node->len = 0;
}
}
if (node->len == 0 && node->next.empty()) {
node->len = 1 + w.size();
res += node->len;
}
}
return res;
}
};
1109. 航班预订统计
这里有 n 个航班,它们分别从 1 到 n 进行编号。
我们这儿有一份航班预订表,表中第 i 条预订记录 bookings[i] = [i, j, k] 意味着我们在从 i 到 j 的每个航班上预订了 k 个座位。
请你返回一个长度为 n 的数组 answer,按航班编号顺序返回每个航班上预订的座位数。
提示:
- 1 <= bookings.length <= 20000
- 1 <= bookings[i][0] <= bookings[i][1] <= n <= 20000
- 1 <= bookings[i][2] <= 10000
解法:
暴力模拟(超时)
看范围就知道会爆了,不过还是手贱试了一下。
代码:
class Solution {
public:
vector<int> corpFlightBookings(vector<vector<int>>& bookings, int n) {
vector<int> a(n+1,0);
for(auto x : bookings){
for(int i=x[0]; i<=x[1]; i++)
a[i] += x[2];
}
a.erase(a.begin());
return a;
}
};
扫描法
- 换一种思路理解题意,将问题转换为:某公交车共有 n 站,第 i 条记录 bookings[i] = [i, j, k] 表示在 i 站上车 k 人,乘坐到 j 站,在 j+1 站下车,需要按照车站顺序返回每一站车上的人数
- 根据 1 的思路,定义 counter[] 数组记录每站的人数变化,counter[i] 表示第 i+1 站。遍历 bookings[]:bookings[i] = [i, j, k] 表示在 i 站增加 k 人即 counters[i-1] += k,在 j+1 站减少 k 人即 counters[j] -= k
- 遍历(整理)counter[] 数组,得到每站总人数: 每站的人数为前一站人数加上当前人数变化 counters[i] += counters[i - 1]
代码:
class Solution {
public:
vector<int> corpFlightBookings(vector<vector<int>>& bookings, int n) {
vector<int> a(n,0);
for(auto x : bookings){
if(x[0]-1>=0)a[x[0]-1] += x[2];
if(x[1]<n)a[x[1]] -= x[2];
}
for(int i=1; i<n; i++){
a[i] += a[i-1];
}
return a;
}
};
423. 从英文中重建数字
给定一个非空字符串,其中包含字母顺序打乱的英文单词表示的数字0-9。按升序输出原始的数字。
注意:
- 输入只包含小写英文字母。
- 输入保证合法并可以转换为原始的数字,这意味着像 “abc” 或 “zerone” 的输入是不允许的。
- 输入字符串的长度小于 50,000。
解法:我用了很笨的方法,就嗯解,写了10个函数,然后再判断顺序修改的时候发现了一些优化的思路,不过已经不想写了,从0-9顺序判断,会出错,因为是恰好全部能转换成数字,这样可能会抢占别的数字的字母,所以我们要先判断一些有唯一字母的数字,然后再判断其他数字,每次都要保证当前数字不会抢占别的数字。
代码:
class Solution {
public:
bool checkZero(vector<int>& a){
if(a[int('z'-'a')]>=1 && a[int('e'-'a')]>=1 && a[int('r'-'a')]>=1 && a[int('o'-'a')]>=1){
a[int('z'-'a')]--;
a[int('e'-'a')]--;
a[int('r'-'a')]--;
a[int('o'-'a')]--;
return true;
}
return false;
}
bool checkOne(vector<int>& a){
if(a[int('o'-'a')]>=1 && a[int('n'-'a')]>=1 && a[int('e'-'a')]>=1){
a[int('o'-'a')]--;
a[int('n'-'a')]--;
a[int('e'-'a')]--;
return true;
}
return false;
}
bool checkTwo(vector<int>& a){
if(a[int('t'-'a')]>=1 && a[int('w'-'a')]>=1 && a[int('o'-'a')]>=1){
a[int('t'-'a')]--;
a[int('w'-'a')]--;
a[int('o'-'a')]--;
return true;
}
return false;
}
bool checkThree(vector<int>& a){
if(a[int('t'-'a')]>=1 && a[int('h'-'a')]>=1 && a[int('t'-'a')]>=1 && a[int('e'-'a')]>=2){
a[int('t'-'a')]--;
a[int('h'-'a')]--;
a[int('r'-'a')]--;
a[int('e'-'a')]-=2;
return true;
}
return false;
}
bool checkFour(vector<int>& a){
if(a[int('f'-'a')]>=1 && a[int('o'-'a')]>=1 && a[int('u'-'a')]>=1 && a[int('r'-'a')]>=1){
a[int('f'-'a')]--;
a[int('o'-'a')]--;
a[int('u'-'a')]--;
a[int('r'-'a')]--;
return true;
}
return false;
}
bool checkFive(vector<int>& a){
if(a[int('f'-'a')]>=1 && a[int('i'-'a')]>=1 && a[int('v'-'a')]>=1 && a[int('e'-'a')]>=1){
a[int('f'-'a')]--;
a[int('i'-'a')]--;
a[int('v'-'a')]--;
a[int('e'-'a')]--;
return true;
}
return false;
}
bool checkSix(vector<int>& a){
if(a[int('s'-'a')]>=1 && a[int('i'-'a')]>=1 && a[int('x'-'a')]>=1){
a[int('s'-'a')]--;
a[int('i'-'a')]--;
a[int('x'-'a')]--;
return true;
}
return false;
}
bool checkSeven(vector<int>& a){
if(a[int('s'-'a')]>=1 && a[int('v'-'a')]>=1 && a[int('n'-'a')]>=1 && a[int('e'-'a')]>=2){
a[int('s'-'a')]--;
a[int('v'-'a')]--;
a[int('n'-'a')]--;
a[int('e'-'a')]-=2;
return true;
}
return false;
}
bool checkEight(vector<int>& a){
if(a[int('e'-'a')]>=1 && a[int('i'-'a')]>=1 && a[int('g'-'a')]>=1 && a[int('h'-'a')]>=1 && a[int('t'-'a')]>=1){
a[int('e'-'a')]--;
a[int('i'-'a')]--;
a[int('g'-'a')]--;
a[int('h'-'a')]--;
a[int('t'-'a')]--;
return true;
}
return false;
}
bool checkNine(vector<int>& a){
if(a[int('i'-'a')]>=1 && a[int('e'-'a')]>=1 && a[int('n'-'a')]>=2){
a[int('i'-'a')]--;
a[int('e'-'a')]--;
a[int('n'-'a')]-=2;
return true;
}
return false;
}
string originalDigits(string s) {
if(s.empty()) return s;
vector<int> a(30,0);
for(char c:s)
a[int(c-'a')]++;
string res = "";
while(checkZero(a))//顺序就按照上面的思路
res += '0';
while(checkSix(a))
res += '6';
while(checkEight(a))
res += '8';
while(checkTwo(a))
res += '2';
while(checkThree(a))
res += '3';
while(checkFour(a))
res += '4';
while(checkFive(a))
res += '5';
while(checkSeven(a))
res += '7';
while(checkOne(a))
res += '1';
while(checkNine(a))
res += '9';
sort(res.begin(),res.end());
return res;
}
};
215. 数组中的第K个最大元素
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
解法:这题是很经典的一题,我们用多种方法来解决。
排序
时间复杂度O(NlogN)的解法,空间复杂度O(1)。
代码:
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
sort(nums.rbegin(),nums.rend());
return nums[k-1];
}
};
堆
可以维护一个小顶堆,然后保持堆大小不超过k,这样遍历结束时,堆顶就是答案。时间:O(Nlogk) 空间:O(k)。
代码:
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
if(k<=0 || nums.empty()) return 0;
priority_queue<int,vector<int>,greater<int> > pq;//小顶堆
for(int i=0; i<nums.size(); i++){
pq.push(nums[i]);
if(pq.size()>k){//保持个数不超过k
pq.pop();
}
}
return pq.top();
}
};
快速选择法
用快徘的思维解决第k小问题。时间:O(N),空间:O(1)。
- 从开头讲起,随机选择一个pivot,将其移动至数组的尾端
- 然后初始化两个指针,一个叫store_index,其值代表当前小于pivot的个数,在数组中,其左边的元素代表小于pivot的元数,其初始值为0;
- 另一个指针j是用来遍历的,起点为store_index,终点为right-1。
- 每次j的数值小于pivot的话,把j和store_index的值互换,store_index+=1,也就是把小于pivot的值放在store_index的左边,然后j继续遍历。
- j从store_index到right-1的遍历结束后,把最终store_index的位置和right(也就是pivot)的位置呼唤,使得此时pivot左边的值都小于pivot,右边的值都大于pivot。
- 然后判断top k_smallest与当前pivot位置的大小关系,若两者相等,则找到目标位置啦,若两者不等,则由其大小关系去判断是要选择pivot的左边还是右边部分继续前面的步骤直到找到目标位置。
代码:
class Solution { //最小堆的方法
public:
int partition(int left, int right, int k_smallest, vector<int>& nums, int pivot_index){
//首先把pivot_index放在最后的位置
int pivot=nums[pivot_index];
int tmp=nums[pivot_index];
nums[pivot_index]=nums[right];
nums[right]=tmp;
//选定当前小于pivot的位置,记为i
int i=left;
for(int j=i;j<right;j++){
if(nums[j]<pivot){
int tmp=nums[j];
nums[j]=nums[i];
nums[i]=tmp;
i+=1;
}
}
tmp=nums[i];
nums[i]=nums[right];
nums[right]=tmp;
return i;
}
int select(int left, int right, int k_smallest, vector<int>& nums){
if(left==right){
return nums[left];
}
int pivot_index = (rand() % (right-left+1))+ left;//随机化pivot,防止数组有序使算法退化
pivot_index = partition(left, right, k_smallest, nums, pivot_index);
if(pivot_index==k_smallest){
return nums[pivot_index];
}else if(k_smallest>pivot_index){
return select(pivot_index+1, right,k_smallest, nums);
}
else{
return select(left,pivot_index-1, k_smallest, nums);
}
}
int findKthLargest(vector<int>& nums, int k) {
return select(0, nums.size()-1, nums.size()-k, nums);
}
};
593. 有效的正方形
给定二维空间中四点的坐标,返回四点是否可以构造一个正方形。
一个点的坐标(x,y)由一个有两个整数的整数数组表示。
注意:
- 所有输入整数都在 [-10000,10000] 范围内。
- 一个有效的正方形有四个等长的正长和四个等角(90度角)。
- 输入点没有顺序。
解法:题目乍一看属实不难,只是情况比较多,繁琐是这题的难点,我们理清一下思路,可以发现有三种情况需要考虑:
- 平90°的正方形,意味着四个边都在坐标线了。
- 斜45°的正方形。
- 其他角度的正方形。
代码:
class Solution {
public:
int sqr(int a){
return a*a;
}
bool validSquare(vector<int>& p1, vector<int>& p2, vector<int>& p3, vector<int>& p4) {
vector<vector<int>> p = {p1,p2,p3,p4};
sort(p.begin(),p.end());
//情况1
if(p[0][0]==p[1][0]){
if(p[0][0]!=p[2][0] && p[2][0]==p[3][0] && abs(p[0][1]-p[1][1])==abs(p[2][1]-p[3][1]) && abs(p[2][0]-p[0][0])==abs(p[0][1]-p[1][1]) && max(p[0][1],p[1][1])==max(p[2][1],p[3][1])){
return true;
}
return false;
}
//情况2
if(p[1][0]==p[2][0]){
if(p[1][0]-p[0][0] == p[3][0]-p[2][0] && abs(p[1][1]-p[0][1])==abs(p[3][1]-p[2][1])){
return true;
}
return false;
}
//情况3
if(double(p[0][0]-p[1][0]) * double(p[0][0]-p[2][0])/double(p[0][1]-p[1][1])/double(p[0][1]-p[2][1])==-1 && double(p[0][0]-p[1][0]) * double(p[1][0]-p[3][0])/double(p[0][1]-p[1][1])/double(p[1][1]-p[3][1])==-1 && double(p[1][0]-p[3][0]) * double(p[3][0]-p[2][0])/double(p[1][1]-p[3][1])/double(p[3][1]-p[2][1])==-1){//三个角是90°
if(sqr(p[0][0]-p[1][0])+sqr(p[0][1]-p[1][1])==sqr(p[0][0]-p[2][0])+sqr(p[0][1]-p[2][1]) && sqr(p[0][0]-p[1][0])+sqr(p[0][1]-p[1][1])==sqr(p[1][0]-p[3][0])+sqr(p[1][1]-p[3][1]))//三条边相等
return true;
}
return false;
}
};
题目小结:这道题让我学到了不少小知识,在这里总结一下:
- 注意我的自定义函数sqr,一开始我写的是int sqr(int& a),然后报错了,主要原因是下面的判断语句里面调用sqr的时候是常数,不是变量,所以不需要引用。
- 一开始我的斜率相乘判断的时候,没有加double,导致出错,int除法会出现0,导致结果错误。
- 仔细看我的斜率判断条件,你会发现我先乘再除,然后不是两个斜率分别算出来相乘,因为double的原因,先除再乘会有误差,如果斜率乘积是-1,先乘再除不会影响结果。
这些小细节的错误反应了我C++基础的不扎实哈哈,所谓细节决定成败,还要多努力,刷的题目越多,基础就会越扎实,加油!