一、链表
struct ListNode {
int val;
struct ListNode* next;
ListNode(int x) :
val(x), next(nullptr) {}
};
1. 反转链表【简单】
给定一个单链表的头结点pHead,长度为n,反转该链表后,返回新链表的表头。要求:空间复杂度O(1),时间复杂度O(n)。
class Solution {
public:
ListNode* ReverseList(ListNode* pHead)
{
ListNode* pre = nullptr;
ListNode* cur = pHead;
ListNode* nex = nullptr;
while (cur)
{
nex = cur->next;
cur->next = pre;
pre = cur;
cur = nex;
}
return pre;
}
};
2. 链表内指定区间反转【中等】
将一个节点数为size链表,把 m 位置到 n 位置之间的区间反转,返回头结点,要求时间复杂度O(n),空间复杂度O(1)。数据范围: 链表长度 0 < size ≤1000,0 < m ≤ n ≤ size,链表中每个节点的值满足∣val∣ ≤ 1000。
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int m, int n)
{
// 加个表头
ListNode* res = new ListNode(-1);
res->next = head;
// 前序节点
ListNode* pre = res;
// 当前节点
ListNode* cur = head;
// 找到 m 号节点
for (int i = 1; i < m; i++)
{
pre = cur;
cur = cur->next;
}
// 从 m 号节点反转到 n 号节点
for (int i = m; i < n; i++)
{
ListNode* temp = cur->next;
cur->next = temp->next;
temp->next = pre->next;
pre->next = temp;
}
// 返回去掉表头的链表
return res->next;
}
};
3. 链表中的节点每k个一组翻转【中等】
将给出的链表中的节点每 k 个一组翻转,返回翻转后的链表。如果链表中的节点数不是 k 的倍数,将最后剩下的节点保持原样。
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k)
{
// 找到每次翻转的尾部
ListNode* tail = head;
// 遍历 k 次到尾部
for (int i = 0; i < k; i++)
{
// 如果不足 k 次到了链表尾,直接返回,不翻转
if (tail == nullptr)
{
return head;
}
tail = tail->next;
}
// 翻转时所需要的前序和当前节点
ListNode* pre = nullptr;
ListNode* cur = head;
// 在到达当前段尾节点前
while (cur != tail)
{
// 翻转
ListNode* temp = cur->next;
cur->next = pre;
pre = cur;
cur = temp;
}
// 当前尾指向下一段要翻转的链表
head->next = reverseKGroup(tail, k);
return pre;
}
};
4. 合并两个排序的链表【简单】
输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。要求:空间复杂度O(1),时间复杂度O(n)
class Solution {
public:
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
if (pHead1 == nullptr)
{
return pHead2;
}
if (pHead2 == nullptr)
{
return pHead1;
}
// 表头
ListNode* result = new ListNode(-1);
ListNode* cur = result;
while (pHead1 && pHead2)
{
if (pHead1->val <= pHead1->val)
{
cur->next = pHead1;
pHead1 = pHead1->next;
}
else
{
cur->next = pHead2;
pHead2 = pHead2->next;
}
cur = cur->next;
}
if (pHead1)
{
cur->next = pHead1;
}
else
{
cur->next = pHead2;
}
return result->next;
}
};
5. 合并k个已排序的链表【困难】
合并 k 个升序的链表并将结果作为一个升序的链表返回其头节点。每个链表的长度满足1 ≤ len ≤ 200,要求:时间复杂度 O(nlogk)
class Solution {
public:
struct Compare
{
// 重载小顶堆比较方式
bool operator()(ListNode* a, ListNode* b)
{
return a->val > b->val;
}
};
ListNode* mergeKLists(vector<ListNode*>& lists)
{
// 小顶堆
priority_queue<ListNode*, vector<ListNode*>, Compare> pq;
// 将所有节点加入小顶堆
for (int i = 0; i < lists.size(); i++)
{
if (lists[i] != nullptr)
{
pq.push(lists[i]);
}
}
// 表头
ListNode* res = new ListNode(-1);
ListNode* cur = res;
while (!pq.empty())
{
// 取堆顶
ListNode* temp = pq.top();
pq.pop();
cur->next = temp;
cur = cur->next;
// 取该节点后一个元素加入小顶堆
if (temp->next != nullptr)
{
pq.push(temp->next);
}
}
return res->next;
}
};
6. 判断链表中是否有环【简单】
判断给定的链表中是否有环(找NULL)。如果有环则返回true,否则返回false。双指针之快慢指针:同方向访问一个链表。双指针之对撞指针:相反方向扫描。
class Solution {
public:
bool hasCycle(ListNode* head)
{
// 快指针 移动两步
ListNode* fast = head;
// 慢指针 移动一步
ListNode* slow = head;
while (fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
// 相遇则有环
if (fast == slow)
{
return true;
}
}
// 无环, 快指针会先到链表尾
return false;
}
};
7. 链表中环的入口结点【中等】
给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回NULL。要求:空间复杂度O(1),时间复杂度O(n)
在慢指针进入链表环之前,快指针已经进入了环,且在里面循环,这才能在慢指针进入环之后,快指针追到了慢指针,不妨假设快指针在环中走了n圈,慢指针在环中走了m圈,它们才相遇,而进入环之前的距离为x,环入口到相遇点的距离为y,相遇点到环入口的距离为z。快指针一共走了x+n(y+z)+y步,慢指针一共走了x+m(y+z)+y,这个时候快指针走的倍数是慢指针的两倍,则x+n(y+z)+y=2(x+m(y+z)+y),这时候x+y=(n−2m)(y+z),因为环的大小是y+z,说明从链表头经过环入口到达相遇地方经过的距离等于整数倍环的大小。
- 判断链表是否有环,并找到相遇的节点。
- 慢指针在相遇节点,快指针回到链表头,两个指针同步逐个元素开始遍历链表。
- 再次相遇的地方就是环的入口。
class Solution {
public:
ListNode* hasCycle(ListNode* head)
{
// 快指针 移动两步
ListNode* fast = head;
// 慢指针 移动一步
ListNode* slow = head;
while (fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
// 相遇则有环
if (fast == slow)
{
return slow;
}
}
// 无环
return nullptr;
}
ListNode* EntryNodeOfLoop(ListNode* pHead)
{
ListNode* ptr1 = pHead;
ListNode* ptr2 = hasCycle(pHead);
if (!ptr2)
{
return nullptr;
}
while (ptr1 != ptr2)
{
ptr1 = ptr1->next;
ptr2 = ptr2->next;
}
return ptr1;
}
};
8. 链表中倒数最后k个结点【简单】
输入一个长度为 n 的链表,设链表中的元素的值为 ai ,返回该链表中倒数第k个节点。如果该链表长度小于k,请返回一个长度为 0 的链表。
- 准备一个快指针,从链表头开始,在链表上先走k步
- 准备慢指针指向原始链表头,代表当前元素,则慢指针与快指针之间的距离一直都是k
- 快慢指针同步移动,当快指针到达链表尾部的时候,慢指针正好到了倒数k个元素的位置
class Solution {
public:
ListNode* FindKthToTail(ListNode* pHead, int k)
{
// 快指针
ListNode* fast = pHead;
// 慢指针
ListNode* slow = pHead;
// 快指针先行k步
for (int i = 0; i < k; i++)
{
if (fast != nullptr)
{
fast = fast->next;
}
// 链表过短,没有倒数第k个节点
else
{
return slow = nullptr;
}
}
// 快慢指针同步,快指针先到底,慢指针指向倒数第k个
while (fast != nullptr)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
};
9. 删除链表的倒数第n个节点【中等】
给定一个链表,删除链表的倒数第 n 个节点并返回链表的头指针。题目保证 n 一定是有效的。
- 给链表添加一个表头,处理删掉第一个元素时比较方便。
- 准备一个快指针,在链表上先走n步。
- 准备慢指针指向原始链表头,代表当前元素,前序节点指向添加的表头,这样两个指针之间相距就是一直都是n。
- 快慢指针同步移动,当快指针到达链表尾部的时候,慢指针正好到了倒数n个元素的位置。
- 最后将该节点前序节点的指针指向该节点后一个节点,删掉这个节点。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n)
{
// 添加表头
ListNode* res = new ListNode(-1);
res->next = head;
// 当前节点
ListNode* cur = head;
// 前序节点
ListNode* pre = res;
ListNode* fast = head;
// 快指针先行n步
while (n--)
{
fast = fast->next;
}
// 快慢指针同步,快指针到达末尾,慢指针就到了倒数第n个位置
while (fast != nullptr)
{
fast = fast->next;
pre = cur;
cur = cur->next;
}
// 删除该位置的节点
pre->next = cur->next;
// 返回去掉头节点
return res->next;
}
};
10. 两个链表的第一个公共结点【简单】
输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。
让N1和N2一起遍历,当N1先走完链表1,则从链表2的头节点继续遍历,同样,如果N2先走完了链表2,则从链表1的头节点继续遍历。因为两个指针,同样的速度,走完同样长度(链表1+链表2),不管两条链表有无相同节点,都能够到达同时到达终点。
- 有公共节点的时候,N1和N2必会相遇,因为长度一样,速度也一定,必会走到相同的地方,所以当两者相等的时候,则会第一个公共的节点
- 无公共节点的时候,此时N1和N2则都会走到终点,那么他们此时都是NULL,所以也算是相等了。
class Solution {
public:
ListNode* FindFirstCommonNode(ListNode* pHead1, ListNode* pHead2)
{
if (pHead1 == nullptr || pHead2 == nullptr)
{
return nullptr;
}
ListNode* ptr1 = pHead1;
ListNode* ptr2 = pHead2;
while (ptr1 != ptr2)
{
if (ptr1 == nullptr)
{
ptr1 = pHead2;
}
else
{
ptr1 = ptr1->next;
}
if (ptr2 == nullptr)
{
ptr2 = pHead1;
}
else
{
ptr2 = ptr2->next;
}
}
return ptr1;
}
};
11. “链表相加”【中等】
假设链表中每一个节点的值都在 0 - 9 之间,那么链表整体就可以代表一个整数。给定两个这种链表,请生成代表两个整数相加值的结果链表。
- 任意一个链表为空,返回另一个链表即可。
- 反转两个待相加的链表。
- 设置返回链表的链表头,设置进位carry=0。
- 从头开始遍历两个链表,直到两个链表节点都为空且carry也不为1。每次取出不为空的链表节点值,为空就设置为0,将两个数字与carry相加,然后查看是否进位,将进位后的结果(对10取模)加入新的链表节点,连接在返回链表后面,并继续往后遍历。
- 返回前将结果链表再反转回来。
class Solution {
public:
//反转链表
ListNode* ReverseList(ListNode* pHead)
{
ListNode* pre = nullptr;
ListNode* cur = pHead;
ListNode* nex = nullptr;
while (cur)
{
nex = cur->next;
cur->next = pre;
pre = cur;
cur = nex;
}
return pre;
}
ListNode* addInList(ListNode* head1, ListNode* head2)
{
// 任一链表为空,返回另一个
if (!head1)
{
return head2;
}
if (!head2)
{
return head1;
}
// 反转两个链表
head1 = ReverseList(head1);
head2 = ReverseList(head2);
// 添加表头
ListNode* res = new ListNode(-1);
ListNode* cur = res;
// 进位符号
int carry = 0;
// 某个链表不为空或者进位不为零
while (head1 || head2 || carry != 0)
{
// 链表不为空则取其值
int val1 = (head1 == nullptr) ? 0 : head1->val;
int val2 = (head2 == nullptr) ? 0 : head2->val;
// 相加
int temp = val1 + val2 + carry;
// 获取进位
carry = temp / 10;
// 获取该位结果
temp %= 10;
// 添加元素
cur->next = new ListNode(temp);
cur = cur->next;
// 移动下一个
if (head1 != nullptr)
{
head1 = head1->next;
}
if (head2 != nullptr)
{
head2 = head2->next;
}
}
// 结果反转回来
return ReverseList(res->next);
}
};
12. 单链表的排序【中等】
给定一个节点数为n的无序单链表,对其按升序排序。
class Solution {
public:
// 把两段有序链表合并为一个有序链表
ListNode* merge(ListNode* pHead1, ListNode* pHead2)
{
// 任一链表为空,返回另一个
if (pHead1 == nullptr)
{
return pHead2;
}
if (pHead2 == nullptr)
{
return pHead1;
}
// 加一个表头
ListNode* head = new ListNode(-1);
ListNode* cur = head;
// 两个链表都要不为空
while (pHead1 && pHead2)
{
// 取较小值的节点
if (pHead1->val <= pHead2->val)
{
cur->next = pHead1;
pHead1 = pHead1->next;
}
else
{
cur->next = pHead2;
pHead2 = pHead2->next;
}
// 指针后移
cur = cur->next;
}
// 有剩的链表直接连在后面
if (pHead1)
{
cur->next = pHead1;
}
if (pHead2)
{
cur->next = pHead2;
}
// 返回值去掉表头
return head->next;
}
ListNode* sortInList(ListNode* head)
{
// 链表为空或者只有一个元素,直接就是有序的
if (head == nullptr || head->next == nullptr)
{
return head;
}
ListNode* pre = nullptr;
ListNode* slow = head;
ListNode* fast = head;
// 找链表中点
while (fast && fast->next)
{
pre = pre->next;
slow = slow->next;
fast = fast->next->next;
}
// 链表从中点断开
pre->next = nullptr;
// 分成两段排序,合并排好序的两段
return merge(sortInList(head), sortInList(slow));
}
};
13. 判断一个链表是否为回文结构【简单】
回文是指该字符串正序逆序完全一致。
- 慢指针每次走一个节点,快指针每次走两个节点,快指针到达链表尾的时候,慢指针刚好到了链表中点。
- 从中点的位置,开始往后将后半段链表反转。
- 左右双指针,左指针从链表头往后遍历,右指针从链表尾往反转后的前遍历,依次比较遇到的值。
class Solution {
public:
// 反转链表
ListNode* reverse(ListNode* head)
{
ListNode* pre = nullptr;
ListNode* cur = head;
ListNode* nex = nullptr;
while (cur)
{
nex = cur->next;
cur->next = pre;
pre = cur;
cur = nex;
}
return pre;
}
bool isPail(ListNode* head)
{
if (head == nullptr)
{
return true;
}
// 快慢指针
ListNode* fast = head;
ListNode* slow = head;
// 双指针找中点
while (fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
}
//中点处反转
ListNode* ptr1 = reverse(slow);
ListNode* ptr2 = head;
while (ptr1 && ptr2)
{
if (ptr1->val != ptr2->val)
{
return false;
}
ptr1 = ptr1->next;
ptr2 = ptr2->next;
}
return true;
}
};
14. 链表的奇偶重排【中等】
给定一个单链表,请设定一个函数,将链表的奇数位节点和偶数位节点分别放在一起,重排后输出。注意是节点的编号而非节点的数值。
- 如果链表为空,不用重排。
- 使用双指针odd和even分别遍历奇数节点和偶数节点,并给偶数节点链表一个头。
- 上述过程,每次遍历两个节点,且even在后面,因此每轮循环用even检查后两个元素是否为NULL,如果不为再进入循环进行上述连接过程。
- 将偶数节点头接在奇数最后一个节点后,再返回头部。
class Solution {
public:
ListNode* oddEvenList(ListNode* head)
{
// 链表为空
if (head == nullptr)
{
return head;
}
// 偶指针
ListNode* even = head->next;
// 奇指针
ListNode* odd = head;
// 偶序列头
ListNode* evenhead = even;
while (even && even->next)
{
odd->next = even->next;
odd = odd->next;
even->next = odd->next;
even = even->next;
}
// 下标为偶数的整体接在下标为奇数的后面
odd->next = evenhead;
return head;
}
};
15. 删除有序链表中重复的元素-1【简单】
删除给出链表中的重复元素(链表中元素从小到大有序),使链表中的所有元素都只出现一次
- 空链表不处理直接返回。
- 使用一个指针遍历链表,如果指针当前节点与下一个节点的值相同,我们就跳过下一个节点,当前节点直接连接下个节点的后一位。
- 如果当前节点与下一个节点值不同,继续往后遍历。
- 循环过程中每次用到了两个节点值,要检查连续两个节点是否为空。
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head)
{
// 空链表
if (head == nullptr)
{
return nullptr;
}
// 遍历指针
ListNode* cur = head;
// 指针当前和下一位不为空
while (cur && cur->next)
{
// 如果当前与下一位相等则忽略下一位
if (cur->val == cur->next->val)
{
cur->next = cur->next->next;
}
// 否则指针正常遍历
else
{
cur = cur->next;
}
}
return head;
}
};
16. 删除有序链表中重复的元素-2【中等】
给出一个升序排序的链表,删除链表中的所有重复出现的元素,只保留原链表中只出现一次的元素。
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head)
{
// 空链表
if (head == nullptr)
{
return nullptr;
}
// 加表头
ListNode* res = new ListNode(-1);
res->next = head;
ListNode* cur = res;
while (cur->next && cur->next->next)
{
// 遇到相邻两个节点值相同
if (cur->next->val == cur->next->next->val)
{
int temp = cur->next->val;
// 将所有相同的都跳过
while (cur->next != nullptr && cur->next->val == temp)
{
cur->next = cur->next->next;
}
}
else
{
cur = cur->next;
}
}
// 返回时去掉表头
return res->next;
}
};
二、折半查找/排序
17. 二分查找
给定一个元素升序的、无重复数字的整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标(下标从 0 开始),否则返回 -1
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int search(vector<int>& nums, int target)
{
int low = 0;
int high = nums.size() - 1;
while (high >= low)
{
int mid = (low + high) / 2;
if (nums[mid] == target)
{
return mid;
}
else if (nums[mid] > target)
{
high = mid - 1;
}
else
{
low = mid + 1;
}
}
return -1;
}
};
18. 二分查找二维数组
在一个二维数组array中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
- 首先获取矩阵的两个边长,判断特殊情况。
- 首先以左下角为起点,若是它小于目标元素,则往右移动去找大的,若是他大于目标元素,则往上移动去找小的。
- 若是移动到了矩阵边界也没找到,说明矩阵中不存在目标值。
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
bool Find(int target, vector<vector<int> > array)
{
if (array.size() == 0)
{
return false;
}
if (array[0].size() == 0)
{
return false;
}
// 行
int n = array.size();
// 列
int m = array[0].size();
// 从最左下角的元素开始往右或往上
for (int i = n - 1, j = 0; i >= 0 && j < m; )
{
// 元素较大,往上走
if (array[i][j] > target)
{
i--;
}
// 元素较小,往右走
else if (array[i][j] < target)
{
j++;
}
else
{
return true;
}
}
return false;
}
};
19. 寻找峰值
给定一个长度为n的数组nums,请你找到峰值并返回其索引。数组可能包含多个峰值,在这种情况下,返回任何一个所在位置即可。峰值元素是指其值严格大于左右相邻值的元素,严格大于即不能有等于。假设 nums[-1] = nums[n] = −∞。
- 二分查找首先从数组首尾开始,每次取中间值,直到首尾相遇。
- 如果中间值的元素大于它右边的元素,说明往右是向下,我们不一定会遇到波峰,那就往左收缩区间。
- 如果中间值小于右边的元素,说明此时往右是向上,向上一定能有波峰,那我们往右收缩区间。
- 最后区间收尾相遇的点一定就是波峰。
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int findPeakElement(vector<int>& nums) {
int left = 0;
int right = nums.size() - 1;
//二分法
while (left < right) {
int mid = (left + right) / 2;
//右边是往下,不一定有坡峰
if (nums[mid] > nums[mid + 1])
right = mid;
//右边是往上,一定能找到波峰
else
left = mid + 1;
}
//其中一个波峰
return right;
}
};
20. 数组中的逆序对
21. 旋转数组的最小数字
有一个长度为 n 的非降序数组,比如[1,2,3,4,5],将它进行旋转,即把一个数组最开始的若干个元素搬到数组的末尾,变成一个旋转数组,比如变成了[3,4,5,1,2],或者[4,5,1,2,3]这样的。请问,给定这样一个旋转数组,求数组中的最小值。
22. 比较版本号
版本号由修订号组成,修订号与修订号之间由一个"."连接。1个修订号可能有多位数字组成,修订号可能包含前导0,且是合法的。例如,1.02.11,0.1,0.2都是合法的版本号。每个版本号至少包含1个修订号。修订号从左到右编号,下标从0开始,最左边的修订号下标为0,下一个修订号下标为1,以此类推。
比较规则:
一. 比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较忽略任何前导零后的整数值。比如"0.1"和"0.01"的版本号是相等的
二. 如果版本号没有指定某个下标处的修订号,则该修订号视为0。例如,“1.1"的版本号小于"1.1.1”。因为"1.1"的版本号相当于"1.1.0",第3位修订号的下标为0,小于1
三. version1 > version2 返回1,如果 version1 < version2 返回-1,不然返回0。
#include <iostream>
#include <string>
using namespace std;
struct ListNode
{
int val;
struct ListNode* next;
ListNode(int x) :
val(x), next(nullptr) {}
};
class Solution {
public:
int compare(string version1, string version2)
{
int n1 = version1.size();
int n2 = version2.size();
int i = 0, j = 0;
// 直到所有字符串结束
while (i < n1 || j < n2)
{
long long num1 = 0;
// 从下一个“.”前截取数字
while (i < n1 && version1[i] != '.')
{
num1 = num1 * 10 + (version1[i] - '0');
i++;
}
// 跳过“.”
i++;
long long num2 = 0;
// 从下一个“.”前截取数字
while (j < n2 && version2[j] != '.')
{
num2 = num2 * 10 + (version2[j] - '0');
j++;
}
// 跳过“.”
j++;
// 比较数字大小
if (num1 > num2)
{
return 1;
}
if (num1 < num2)
{
return -1;
}
}
// 版本号相同
return 0;
}
};
三、二叉树
struct TreeNode {
int val;
struct TreeNode* left;
struct TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
23. 二叉树的前序遍历【简单】
// 递归
class Solution {
public:
void preorder(vector<int>& res, TreeNode* root)
{
// 遇到空节点则返回
if (root == nullptr)
{
return;
}
// 根节点
res.push_back(root->val);
// 左子树
preorder(res, root->left);
// 右子树
preorder(res, root->right);
}
vector<int> preorderTraversal(TreeNode* root)
{
vector<int> res;
// 递归前序遍历
preorder(res, root);
return res;
}
};
二叉树前序遍历的非递归版本
- 优先判断树是否为空,空树不遍历。
- 准备辅助栈,首先记录根节点。
- 每次从栈中弹出一个元素,进行访问,然后验证该节点的左右子节点是否存在,存的话的加入栈中,优先加入右节点。
// 非递归
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root)
{
vector<int> res;
// 空树
if (root == nullptr)
{
return res;
}
// 辅助栈
stack<TreeNode*> s;
// 根节点先进栈
s.push(root);
// 直到栈中没有节点
while (!s.empty())
{
// 每次栈顶就是访问的元素
TreeNode* node = s.top();
s.pop();
res.push_back(node->val);
// 如果右边还有右子节点进栈
if (node->right)
{
s.push(node->right);
}
// 如果左边还有左子节点进栈
if (node->left)
{
s.push(node->left);
}
}
return res;
}
};
24. 二叉树的中序遍历【中等】
// 递归
class Solution {
public:
void inorder(vector<int>& res, TreeNode* root) {
//遇到空节点则返回
if (root == nullptr)
{
return;
}
// 左子树
inorder(res, root->left);
// 根节点
res.push_back(root->val);
// 右子树
inorder(res, root->right);
}
vector<int> inorderTraversal(TreeNode* root)
{
vector<int> res;
// 递归中序遍历
inorder(res, root);
return res;
}
};
二叉树的中序遍历的非递归版本
- 优先判断树是否为空,空树不遍历。
- 准备辅助栈,当二叉树节点为空了且栈中没有节点了,我们就停止访问。
- 从根节点开始,每次优先进入每棵的子树的最左边一个节点,我们将其不断加入栈中,用来保存父问题。
- 到达最左后,可以开始访问,如果它还有右节点,则将右边也加入栈中,之后右子树的访问也是优先到最左。
// 非递归
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root)
{
vector<int> res;
// 辅助栈
stack<TreeNode*> s;
// 当树节点不为空或栈中有节点时
while (root != nullptr || !s.empty())
{
// 每次找到最左节点
while (root != nullptr)
{
s.push(root);
root = root->left;
}
// 访问该节点
TreeNode* node = s.top();
s.pop();
res.push_back(node->val);
// 进入右节点
root = node->right;
}
return res;
}
};
25. 二叉树的后序遍历【简单】
class Solution {
public:
void postorder(vector<int>& res, TreeNode* root)
{
//遇到空节点则返回
if (root == nullptr)
{
return;
}
// 左子树
postorder(res, root->left);
// 右子树
postorder(res, root->right);
// 根节点
res.push_back(root->val);
}
vector<int> postorderTraversal(TreeNode* root)
{
vector<int> res;
// 递归后序遍历
postorder(res, root);
return res;
}
};
二叉树的后序遍历的非递归版本
- 开辟一个辅助栈,用于记录要访问的子节点,开辟一个前序指针pre。
- 从根节点开始,每次优先进入每棵的子树的最左边一个节点,我们将其不断加入栈中,用来保存父问题。
- 弹出一个栈元素,看成该子树的根,判断这个根的右边有没有节点或是有没有被访问过,如果没有右节点或是 被访问过了,可以访问这个根,并将前序节点标记为这个根。
- 如果没有被访问,那这个根必须入栈,进入右子树继续访问,只有右子树结束了回到这里才能继续访问根。
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root)
{
vector<int> res;
//辅助栈
stack<TreeNode*> s;
TreeNode* pre = nullptr;
while (root != nullptr || !s.empty())
{
// 每次先找到最左边的节点
while (root != nullptr)
{
s.push(root);
root = root->left;
}
// 弹出栈顶
TreeNode* node = s.top();
s.pop();
// 如果该元素的右边没有或是已经访问过
if (node->right == nullptr || node->right == pre)
{
// 访问中间的节点
res.push_back(node->val);
// 且记录为访问过了
pre = node;
}
else
{
// 该节点入栈
s.push(node);
// 先访问右边
root = node->right;
}
}
return res;
}
};
26. 二叉树的层序遍历【中等】
class Solution {
public:
vector<vector<int>> preorderTraversal(TreeNode* root)
{
vector<vector<int>> res;
// 空树
if (root == nullptr)
{
return res;
}
// 辅助队列
queue<TreeNode*> q;
q.push(root);
while (!q.empty())
{
int size = q.size();
// 记录二叉树的某一层
vector<int> temp;
for (int i = 0; i < size; i++)
{
TreeNode* node = q.front();
q.pop();
temp.push_back(node->val);
// 若是左右孩子存在,则存入左右孩子作为下一个层次
if (node->left != nullptr)
{
q.push(node->left);
}
if (node->right != nullptr)
{
q.push(node->right);
}
}
res.push_back(temp);
}
return res;
}
};
27. 按之字形顺序打印二叉树【中等】
给定一个二叉树,返回该二叉树的之字形层序遍历,即第一层从左向右,下一层从右向左,一直这样交替。
class Solution {
public:
vector<vector<int> > Print(TreeNode* pRoot)
{
vector<vector<int>> res;
// 空树
if (pRoot == nullptr)
{
return res;
}
// 辅助队列
queue<TreeNode*> q;
q.push(pRoot);
// 标志位
bool flag = false;
while (!q.empty())
{
// 记录二叉树的某一行
vector<int> temp;
int size = q.size();
for (int i = 0; i < size; i++)
{
TreeNode* node = q.front();
q.pop();
temp.push_back(node->val);
if (node->left != nullptr)
{
q.push(node->left);
}
if (node->right != nullptr)
{
q.push(node->right);
}
}
if (flag)
{
reverse(temp.begin(), temp.end());
}
// 标志位取反
flag = !flag;
res.push_back(temp);
}
return res;
}
};
28. 二叉树的最大深度【简单】
求给定二叉树的最大深度,深度是指树的根节点到任一叶子节点路径上节点的数量。最大深度是所有叶子节点的深度的最大值。(注:叶子节点是指没有子节点的节点)
// 递归
class Solution {
public:
int maxDepth(TreeNode* root)
{
// 空节点没有深度
if(root == nullptr)
{
return 0;
}
// 返回子树深度+1
return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}
};
// 层次遍历
class Solution {
public:
int maxDepth(TreeNode* root)
{
// 空树
if (root == NULL)
{
return 0;
}
// 辅助队列
queue<TreeNode*> q;
// 根节点入队
q.push(root);
// 记录深度
int res = 0;
// 层次遍历
while (!q.empty())
{
// 记录当前层有多少节点
int n = q.size();
// 遍历完这一层,再进入下一层
for (int i = 0; i < n; i++)
{
TreeNode* node = q.front();
q.pop();
// 添加下一层的左右节点
if (node->left)
{
q.push(node->left);
}
if (node->right)
{
q.push(node->right);
}
}
// 深度+1
res++;
}
return res;
}
};
29. 二叉树中和为某一值的路径【简单】
给定一个二叉树 root 和一个值 sum ,判断是否有从根节点到叶子节点的节点值之和等于 sum 的路径。
1.该题路径定义为从树的根结点开始往下一直到叶子结点所经过的结点
2.叶子节点是指没有子节点的节点
3.路径只能从父节点到子节点,不能从子节点到父节点
4.总节点数目为n
// 栈 + 深度优先搜索
class Solution {
public:
bool hasPathSum(TreeNode* root, int sum)
{
// 空树
if (root == nullptr)
{
return false;
}
// 辅助栈
stack<pair<TreeNode*, int>> s;
// 根节点入栈
s.push({ root, root->val });
while (!s.empty())
{
auto temp = s.top();
s.pop();
// 叶子节点且路径和等于sum
if (temp.first->left == nullptr && temp.first->right == nullptr && temp.second == sum)
{
return true;
}
// 左节点入栈
if (temp.first->left != nullptr)
{
s.push({ temp.first->left, temp.second + temp.first->left->val });
}
// 右节点入栈
if (temp.first->right != nullptr)
{
s.push({ temp.first->right, temp.second + temp.first->right->val });
}
}
return false;
}
};
30. 二叉搜索树与双向链表【中等】
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表,即:二叉搜索树转化成递增序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。如下图所示:
class Solution {
public:
TreeNode* Convert(TreeNode* pRootOfTree)
{
// 空树
if (pRootOfTree == nullptr)
{
return nullptr;
}
// 辅助栈
stack<TreeNode*> s;
TreeNode* head = nullptr;
TreeNode* pre = nullptr;
// 是否第一次到最左
bool isFirst = true;
while (pRootOfTree != nullptr || !s.empty())
{
// 直到没有左节点
while (pRootOfTree != nullptr)
{
s.push(pRootOfTree);
pRootOfTree = pRootOfTree->left;
}
pRootOfTree = s.top();
s.pop();
// 首位
if (isFirst)
{
head = pRootOfTree;
pre = head;
isFirst = false;
}
else
{
pre->right = pRootOfTree;
pRootOfTree->left = pre;
pre = pRootOfTree;
}
pRootOfTree = pRootOfTree->right;
}
return head;
}
};
31. 对称的二叉树【简单】
给定一棵二叉树,判断其是否对称。必须包含空节点!
// 层次遍历
class Solution {
public:
bool isSymmetrical(TreeNode* pRoot)
{
// 空树
if (pRoot == nullptr)
{
return true;
}
// 辅助队列
queue<TreeNode*> q1;
queue<TreeNode*> q2;
q1.push(pRoot->left);
q2.push(pRoot->right);
while (!q1.empty() && !q2.empty())
{
// 分别从左边和右边弹出节点
TreeNode* left = q1.front();
q1.pop();
TreeNode* right = q2.front();
q2.pop();
// 都为空暂时对称
if (left == nullptr && right == nullptr)
{
continue;
}
// 某一个为空或者数字不相等则不对称
if (left == nullptr || right == nullptr || left->val != right->val)
{
return false;
}
// 从左往右加入队列
q1.push(left->left);
q1.push(left->right);
// 从右往左加入队列
q2.push(right->right);
q2.push(right->left);
}
// 都检验完都是对称的
return true;
}
};
32. 合并二叉树【简单】
已知两颗二叉树,将它们合并成一颗二叉树。合并规则是:都存在的结点,就将结点值加起来,否则空的位置就由另一个树的结点来代替。例如:
// 层次遍历
class Solution {
public:
TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2)
{
// 空树
if (t1 == nullptr)
{
return t2;
}
if (t2 == nullptr)
{
return t1;
}
// 合并根节点
TreeNode* head = new TreeNode(t1->val + t2->val);
// 合并后的树的辅助队列
queue<TreeNode*> q;
// 待合并的两棵树的辅助队列
queue<TreeNode*> q1;
queue<TreeNode*> q2;
q.push(head);
q1.push(t1);
q2.push(t2);
while (!q1.empty() && !q2.empty())
{
TreeNode* node = q.front(), * node1 = q1.front(), * node2 = q2.front();
q.pop();
q1.pop();
q2.pop();
TreeNode* left1 = node1->left, * left2 = node2->left, * right1 = node1->right, * right2 = node2->right;
// 左节点
if (left1 || left2)
{
// 左节点都存在
if (left1 && left2)
{
TreeNode* left = new TreeNode(left1->val + left2->val);
node->left = left;
// 新节点入队列
q.push(left);
q1.push(left1);
q2.push(left2);
}
// 只存在一个左节点
else if (left1)
{
node->left = left1;
}
else if (left2)
{
node->left = left2;
}
}
// 右节点
if (right1 || right2)
{
// 右节点都存在
if (right1 && right2)
{
TreeNode* right = new TreeNode(right1->val + right2->val);
node->right = right;
//新节点入队列
q.push(right);
q1.push(right1);
q2.push(right2);
}
// 只存在一个右节点
else if (right1)
{
node->right = right1;
}
else
{
node->right = right2;
}
}
}
return head;
}
};
33. 二叉树的镜像【简单】
操作给定的二叉树,将其变换为源二叉树的镜像。
- 优先检查空树的情况。
- 使用栈辅助遍历二叉树,根节点先进栈。
- 遍历过程中每次弹出栈中一个元素,然后该节点左右节点分别入栈。
- 同时我们交换入栈两个子节点的值,因为子节点已经入栈了再交换,就不怕后续没有交换。
// 栈
class Solution {
public:
TreeNode* Mirror(TreeNode* pRoot)
{
// 空树
if (pRoot == nullptr)
{
return nullptr;
}
// 辅助栈
stack<TreeNode*> s;
// 根节点先进栈
s.push(pRoot);
while (!s.empty())
{
TreeNode* node = s.top();
s.pop();
// 左右节点入栈
if (node->left != nullptr)
{
s.push(node->left);
}
if (node->right != nullptr)
{
s.push(node->right);
}
// 交换左右
TreeNode* temp = node->left;
node->left = node->right;
node->right = temp;
}
return pRoot;
}
};
34. 判断是不是二叉搜索树【中等】
二叉搜索树满足每个节点的左子树上的所有节点均严格小于当前节点且右子树上的所有节点均严格大于当前节点。
// 中序遍历
class Solution {
public:
bool isValidBST(TreeNode* root)
{
// 设置栈用于遍历
stack<TreeNode*> s;
TreeNode* head = root;
// 记录中序遍历的结果
vector<int> res;
while (head != nullptr || !s.empty())
{
// 直到没有左节点
while (head != nullptr)
{
s.push(head);
head = head->left;
}
head = s.top();
s.pop();
// 访问节点
res.push_back(head->val);
head = head->right;
}
// 遍历中序结果
for (int i = 1; i < res.size(); i++)
{
// 一旦有降序,则不是搜索树
if (res[i - 1] > res[i])
{
return false;
}
}
return true;
}
};
35. 判断是不是完全二叉树【中等】
完全二叉树的定义:若二叉树的深度为 h,除第 h 层外,其它各层的结点数都达到最大个数,第 h 层所有的叶子结点都连续集中在最左边,这就是完全二叉树。
// 层次遍历
class Solution {
public:
bool isCompleteTree(TreeNode* root)
{
// 空树
if (root == nullptr)
{
return true;
}
// 辅助队列
queue<TreeNode*> q;
// 根节点先访问
q.push(root);
// 定义一个首次出现的标记位
bool flag = false;
//层次遍历
while (!q.empty())
{
int size = q.size();
for (int i = 0; i < size; i++)
{
TreeNode* cur = q.front();
q.pop();
// 标记第一次遇到空节点
if (cur == nullptr)
{
flag = true;
}
else
{
// 已经遇到空节点了,后续访问还有叶子节点
if (flag)
{
return false;
}
q.push(cur->left);
q.push(cur->right);
}
}
}
return true;
}
};
36. 判断是不是平衡二叉树【简单】
平衡二叉树具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
class Solution {
public:
// 计算二叉树深度
int deep(TreeNode* root)
{
// 空树
if (root == nullptr)
{
return 0;
}
// 左子树深度
int left = deep(root->left);
// 右子树深度
int right = deep(root->right);
// 子树最大深度+1
return max(left, right) + 1;
}
bool IsBalanced_Solution(TreeNode* pRoot)
{
// 空树
if (pRoot == nullptr)
{
return true;
}
int left = deep(pRoot->left);
int right = deep(pRoot->right);
// 平衡因子绝对值大于1
if (abs(left - right) > 1)
{
return false;
}
// 左右子树也必须是平衡的
return IsBalanced_Solution(pRoot->left) && IsBalanced_Solution(pRoot->right);
}
};
37. 二叉搜索树的最近公共祖先【简单】
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。公共祖先定义:对于有根树T的两个节点p、q,最近公共祖先节点x,满足x是p和q的祖先且x的深度尽可能大。在这里,一个节点也可以是它自己的祖先。
题目保证:p、q 为不同节点且均存在于给定的二叉搜索树中。
class Solution {
public:
// 求得根节点到目标节点的路径
vector<int> getPath(TreeNode* root, int target)
{
vector<int> path;
TreeNode* node = root;
// 二叉排序树节点值唯一,可以直接比较
while (node->val != target)
{
path.push_back(node->val);
// 小 左子树
if (target < node->val)
{
node = node->left;
}
// 大 右子树
else
{
node = node->right;
}
}
path.push_back(node->val);
return path;
}
int lowestCommonAncestor(TreeNode* root, int p, int q)
{
// 求根节点到两个节点的路径
vector<int> path_p = getPath(root, p);
vector<int> path_q = getPath(root, q);
int res;
// 比较两个路径,最后一个相同的节点就是最近公共祖先
for (int i = 0; i < path_p.size() && i < path_q.size(); i++)
{
if (path_p[i] == path_q[i])
{
res = path_p[i];
}
else
{
break;
}
}
return res;
}
};
38. 在二叉树中找到两个节点的最近公共祖先
给定一棵二叉树(保证非空)以及这棵树上的两个节点对应的val值 o1 和 o2,请找到 o1 和 o2 的最近公共祖先节点。本题保证:二叉树中每个节点的值均不相同。
class Solution {
public:
// 是否找到tar的标志位
bool flag = false;
// 求根节点到目标节点的路径
void dfs(TreeNode* root, vector<int>& path, int tar)
{
if (flag || root == nullptr)
{
return;
}
path.push_back(root->val);
// 二叉树中每个节点的值均不相同,可以直接用值比较
if (root->val == tar)
{
flag = true;
return;
}
dfs(root->left, path, tar);
dfs(root->right, path, tar);
if (flag)
{
return;
}
// 该子树没有,回溯
path.pop_back();
}
int lowestCommonAncestor(TreeNode* root, int o1, int o2)
{
vector<int> path1;
vector<int> path2;
dfs(root, path1, o1);
// 重置标志位
flag = false;
dfs(root, path2, o2);
int res;
// 最近公共祖先:最后一个相同的节点
for (int i = 0; i < path1.size() && i < path2.size(); i++)
{
if (path1[i] == path2[i])
{
res = path1[i];
}
else
{
break;
}
}
return res;
}
};
四、堆 / 栈 / 队列
42. 用两个栈实现队列【简单】
用两个栈来实现一个队列,实现队列尾部插入数据、队列头部删除数据的功能。 队列中的元素为int类型,题目保证操作合法,即保证pop操作时队列内已有元素。
class Solution
{
public:
// 入队列就正常入栈
void push(int node)
{
stack1.push(node);
}
int pop()
{
// 将第一个栈中内容弹出放入第二个栈中
while (!stack1.empty())
{
stack2.push(stack1.top());
stack1.pop();
}
// 第二个栈栈顶就是最先进来的元素,即队首
int res = stack2.top();
stack2.pop();
// 再将第二个栈的元素放回第一个栈
while (!stack2.empty())
{
stack1.push(stack2.top());
stack2.pop();
}
return res;
}
private:
stack<int> stack1;
stack<int> stack2;
};
43. 包含min函数的栈【简单】
定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的 min 函数,输入操作时保证 pop、top 和 min 函数操作时,栈中一定有元素。
- 使用一个栈记录进入栈的元素,正常进行push、pop、top操作。
- 使用另一个栈记录每次push进入的最小值。
- 每次push元素的时候与第二个栈的栈顶元素比较,若是较小,则进入第二个栈,若是较大,则第二个栈的栈顶元素再次入栈,因为即便加了一个元素,它依然是最小值。于是,每次访问最小值即访问第二个栈的栈顶。
class Solution {
public:
// 用于栈的 push 与 pop
stack<int> s1;
// 用于存储最小值
stack<int> s2;
void push(int value)
{
s1.push(value);
// 栈空或者新元素较小,则入栈
if (s2.empty() || s2.top() > value)
{
s2.push(value);
}
// **最小值重复加入栈顶**
else
{
s2.push(s2.top());
}
}
void pop()
{
s1.pop();
s2.pop();
}
int top()
{
return s1.top();
}
int min()
{
return s2.top();
}
};
44. 有效括号序列【简单】
给出一个仅包含字符’(’ , ‘)’ , ‘{’ , ‘}’ , ‘[’ 和 ‘]’ ,的字符串,判断给出的字符串是否是合法的括号序列。括号必须以正确的顺序关闭,"()“和”()[]{}“都是合法的括号序列,但”(]“和”([)]"不合法。
思路:括号的匹配规则应该符合先进后出原理,最早出现的左括号,也对应最晚出现的右括号,因此可以使用栈。遇到左括号就将相应匹配的右括号加入栈中,后续如果是合法的,右括号来的顺序就是栈中弹出的顺序。
class Solution {
public:
bool isValid(string s)
{
// 辅助栈
stack<char> st;
//遍历字符串
for (int i = 0; i < s.length(); i++)
{
// 遇到左小括号
if (s[i] == '(')
{
// 期待遇到右小括号
st.push(')');
}
// 遇到左中括号
else if (s[i] == '[')
{
// 期待遇到右中括号
st.push(']');
}
// 遇到左大括号
else if (s[i] == '{')
{
// 期待遇到右打括号
st.push('}');
}
// 栈空
else if (st.empty())
{
// 必须有左括号才能遇到右括号
return false;
}
// 遇到右括号
else if (st.top() == s[i])
{
// 匹配则弹出
st.pop();
}
}
//栈中是否还有元素
return st.empty();
}
};
45. 滑动窗口的最大值【困难】
给定一个长度为 n 的数组 nums 和滑动窗口的大小 size ,找出所有滑动窗口里数值的最大值。
// 暴力法
class Solution {
public:
vector<int> maxInWindows(const vector<int>& num, unsigned int size)
{
vector<int> res;
// 窗口长度 > 数组长度 || 窗口长度 <= 0
if (size > num.size() || size <= 0)
{
return res;
}
for (int i = 0; i <= num.size() - size; i++)
{
int max = 0;
for (int j = i; j < i + size; j++)
{
if (num[j] > max)
{
max = num[j];
}
}
res.push_back(max);
}
return res;
}
};
46. 最小的K个数【中等】
给定一个长度为 n 的可能有重复值的数组,找出其中不去重的最小的 k 个数。例如数组元素是4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4(任意顺序皆可)。
思路:建立一个容量为k的大根堆的优先队列。遍历一遍元素,如果队列大小<k,就直接入队,否则,让当前元素与队顶元素相比,如果队顶元素大,则出队,将当前元素入队。
class Solution {
public:
vector<int> GetLeastNumbers_Solution(vector<int> input, int k)
{
vector<int> ret;
if (k == 0 || k > input.size())
{
return ret;
}
// 优先队列 大顶堆
priority_queue<int, vector<int>> pq;
for (const int val : input)
{
if (pq.size() < k)
{
pq.push(val);
}
else
{
// 当前的数比堆顶的数大
if (val < pq.top())
{
pq.pop();
pq.push(val);
}
}
}
while (!pq.empty())
{
ret.push_back(pq.top());
pq.pop();
}
return ret;
}
};
47. 寻找第K大的数
有一个整数数组,请你根据快速排序的思路,找出数组中第 k 大的数。给定一个整数数组 a ,同时给定它的大小n和要找的 k ,请返回第 k 大的数(包括重复的元素,不用去重),保证答案存在。
思路:快速排序,每次移动,可以找到一个标杆元素,然后将大于它的移到左边,小于它的移到右边,由此整个数组划分成为两部分,然后分别对左边和右边使用同样的方法进行排序,不断划分左右子段,直到整个数组有序。这也是分治的思想,将数组分化成为子段,分而治之。
class Solution {
public:
// 常规的快排划分,但这次是大数在左
int partion(vector<int>& a, int low, int high)
{
// 基准
int temp = a[low];
while (low < high)
{
// 小于基准的在右
while (low < high && a[high] <= temp)
{
high--;
}
if (low == high)
{
break;
}
else
{
a[low] = a[high];
}
// 大于基准的在左
while (low < high && a[low] >= temp)
{
low++;
}
if (low == high)
{
break;
}
else
{
a[high] = a[low];
}
}
// 结束条件:low=high
a[low] = temp;
return low;
}
int quickSort(vector<int>& a, int low, int high, int K)
{
// 快排
int p = partion(a, low, high);
// 第K大
if (K == p - low + 1)
{
return a[p];
}
// 第K大在左边
else if (p - low + 1 > K)
{
return quickSort(a, low, p - 1, K);
}
// 第K大在右边
else
{
// 减去左边更大的数字的数量!
return quickSort(a, p + 1, high, K - (p - low + 1));
}
}
int findKth(vector<int> a, int n, int K)
{
return quickSort(a, 0, n - 1, K);
}
};
48. 数据流中的中位数
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
思路:每次都在一个有序数组中插入一个数据,因此可以用插入排序。
class Solution {
public:
#define SCD static_cast<double>
vector<int> vec;
void Insert(int num)
{
if (vec.empty())
{
vec.push_back(num);
}
else
{
// 从vecector容器[begin, end)中二分查找,第一个≥num的位置,不存在则返回end
auto it = lower_bound(vec.begin(), vec.end(), num);
vec.insert(it, num);
}
}
double GetMedian()
{
int size = vec.size();
// 长度是奇数
if (size & 1)
{
return SCD(vec[size >> 1]);
}
// 长度是偶数
else
{
return SCD(vec[size >> 1] + vec[(size - 1) >> 1]) / 2;
}
}
};
49. 表达式求值
请写一个整数计算器,支持加减乘三种运算和括号。
五、哈希
50. 两数之和
给出一个整型数组 numbers 和一个目标值 target,请在数组中找出两个加起来等于目标值的数的下标,返回的下标按升序排列。(注:返回的数组下标从1开始算起,保证target一定可以由数组里面2个数字相加得到)
- 构建一个哈希表,其中key值为遍历数组过程中出现过的值,value值为其相应的下标,因为我们最终要返回的是下标。
- 遍历数组每个元素,如果目标值减去该元素的结果在哈希表中存在,说明我们先前遍历的时候它出现过,根据记录的下标,就可以得到结果。
- 如果相减后的结果没有在哈希表中,说明先前遍历的元素中没有它对应的另一个值,那我们将它加入哈希表,等待后续它匹配的那个值出现即可。
- 需要注意最后的结果是下标值加1。
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target)
{
vector<int> res;
// 哈希表 (值,下标)
unordered_map<int, int> hash;
//在哈希表中查找target-numbers[i]
for (int i = 0; i < numbers.size(); i++)
{
int temp = target - numbers[i];
// 哈希表中没找到,记入哈希表
if (hash.find(temp) == hash.end())
{
hash[numbers[i]] = i;
}
// 哈希表中找到
else
{
res.push_back(hash[temp] + 1);
res.push_back(i + 1);
break;
}
}
return res;
}
};
51. 数组中出现次数超过一半的数字
给一个长度为 n 的数组,数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int MoreThanHalfNum_Solution(vector<int> numbers)
{
sort(numbers.begin(), numbers.end());
int cond = numbers[numbers.size() / 2];
int cnt = 0;
for (const int k : numbers)
{
if (cond == k)
{
++cnt;
}
}
if (cnt > numbers.size() / 2)
{
return cond;
}
return 0;
}
};
52. 数组中只出现一次的两个数字
一个整型数组里除了两个数字只出现一次,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
// 哈希表
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
class Solution {
public:
vector<int> FindNumsAppearOnce(vector<int>& array)
{
unordered_map<int, int> mp;
vector<int> res;
// 统计每个数出现的频率
for (int i = 0; i < array.size(); i++)
{
mp[array[i]]++;
}
// 找到频率为1的两个数
for (int i = 0; i < array.size(); i++)
{
if (mp[array[i]] == 1)
{
res.push_back(array[i]);
}
}
// 整理次序
if (res[0] < res[1])
{
return res;
}
else
{
return { res[1], res[0] };
}
}
};
异或运算满足交换率,且相同的数字作异或会被抵消掉,比如:a⊕b⊕c⊕b⊕c=a,且任何数字与0异或还是原数字,放到这个题目里面所有数字异或运算就会得到a⊕b,也即得到了两个只出现一次的数字的异或和。但是我们是要将其分开得到结果的,可以考虑将数组分成两部分,一部分为a⊕d⊕c⊕d⊕c=a,另一部分为b⊕x⊕y⊕x⊕y=b的样式,怎么划分?因为两个数字不相同,我们就以两个数字异或的第一个为1的位来划分上述的两个数组,相同的数字自然会被划分到另一边,而a与b也会刚好被分开。
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
vector<int> FindNumsAppearOnce(vector<int>& array)
{
vector<int> res(2, 0);
// a⊕b
int temp = 0;
for (int i = 0; i < array.size(); i++)
{
temp ^= array[i];
}
int k = 1;
// 找到两个数第一个不相同的位
while ((k & temp) == 0)
{
k <<= 1;
}
for (int i = 0; i < array.size(); i++)
{
// 对每个数分类
if ((k & array[i]) == 0)
{
res[0] ^= array[i];
}
else
{
res[1] ^= array[i];
}
}
// 整理次序
if (res[0] < res[1])
{
return res;
}
else
{
return { res[1], res[0] };
}
}
};
53. 缺失的第一个正整数
给定一个未排序的整数数组 nums,请你找出其中没有出现的最小的正整数。
思路:n个长度的数组,没有重复,则如果数组填满了1~n,那么缺失n+1,如果数组填不满1~n,那么缺失的就是1~n中的数字。因此只要数字1~n中某个数字出现,我们就可以将对应下标的值做一个标记,最后没有被标记的下标就是缺失的值。
具体做法:
// 原地哈希
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int minNumberDisappeared(vector<int>& nums)
{
int n = nums.size();
// 负数元素全部记为n+1
for (int i = 0; i < n; i++)
{
if (nums[i] <= 0)
{
nums[i] = n + 1;
}
}
for (int i = 0; i < n; i++)
{
if (abs(nums[i]) <= n)
{
// 该数字的下标标记为负数
nums[abs(nums[i]) - 1] = -1 * abs(nums[abs(nums[i]) - 1]);
}
}
// 找到第一个元素不为负数的下标
for (int i = 0; i < n; i++)
{
if (nums[i] > 0)
{
return i + 1;
}
}
return n + 1;
}
};
54. 三数之和
给出一个有n个元素的数组S,S中是否有元素a,b,c满足a+b+c=0?找出数组S中所有满足条件的三元组。
七、动态规划
62. 斐波那契数列【入门】
大家都知道斐波那契数列,现在要求输入一个正整数 n ,请你输出斐波那契数列的第 n 项。
class Solution {
public:
int dp[50] = { 0 };
int Fibonacci(int n)
{
dp[1] = 1, dp[2] = 1;
for (int i = 3; i <= n; i++)
{
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
63. 跳台阶【简单】
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法,先后次序不同算不同的结果。
思路:逆向思维。如果我从第n个台阶进行下台阶,下一步有两种可能,一种走到第n-1个台阶,一种是走到第n-2个台阶。所以f[n] = f[n-1] + f[n-2]。那么初始条件,f[0] = f[1] = 1。
class Solution {
public:
int dp[50] = { 0 };
int jumpFloor(int number)
{
dp[0] = 1, dp[1] = 1;
for (int i = 2; i <= number; i++)
{
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[number];
}
};
64. 最小花费爬楼梯【简单】
给定一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用,下标从0开始。一旦你支付此费用,即可选择向上爬一个或者两个台阶。你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。
- 可以用一个数组记录每次爬到第i阶楼梯的最小花费,然后每增加一级台阶就转移一次状态,最终得到结果。
- 【初始状态】因为可以直接从第0级或是第1级台阶开始,因此这两级的花费都直接为0。
- 【状态转移】每次到一个台阶,只有两种情况,要么是它前一级台阶向上一步,要么是它前两级的台阶向上两步,因为在前面的台阶花费我们都得到了,因此每次更新最小值即可,转移方程为:dp[i] = min(dp[i−1]+cost[i−1], dp[i−2]+cost[i−2])
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost)
{
// dp[i]表示爬到第 i 阶楼梯需要的最小花费
vector<int> dp(cost.size() + 1, 0);
// 每次选取最小的方案
for (int i = 2; i <= cost.size(); i++)
{
dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
return dp[cost.size()];
}
};
65. 最长公共子序列
给定两个字符串str1和str2,输出两个字符串的最长公共子序列。如果最长公共子序列为空,则返回"-1"。目前给出的数据,仅仅会存在一个最长的公共子序列。注意:子序列不是子串,子串要求所有字符必须连续,子序列不要求连续,只要求相对位置不变。
66. 最长公共子串
给定两个字符串 str1 和 str2 ,输出两个字符串的最长公共子串题目保证 str1 和 str2 的最长公共子串存在且唯一。
67. 不同路径的数目【简单】
一个机器人在m×n大小的地图的左上角(起点)。机器人每次可以向下或向右移动。机器人要到达地图的右下角(终点)。可以有多少种不同的路径从起点走到终点?
class Solution {
public:
int uniquePaths(int m, int n)
{
// dp[i][j]表示大小为 i*j 的矩阵的路径数量
vector<vector<int> > dp(m + 1, vector<int>(n + 1, 0));
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
// 只有1行的时候,只有一种路径
if (i == 1)
{
dp[i][j] = 1;
continue;
}
// 只有1列的时候,只有一种路径
if (j == 1)
{
dp[i][j] = 1;
continue;
}
// 路径数等于左方格子的路径数加上上方格子的路径数
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m][n];
}
};
68. 矩阵的最小路径和
给定一个 n * m 的矩阵 a,从左上角开始每次只能向右或者向下走,最后到达右下角的位置,路径上所有的数字累加起来就是路径和,输出所有的路径中最小的路径和。
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int minPathSum(vector<vector<int> >& matrix)
{
// 列
int n = matrix.size();
// 行
int m = matrix[0].size();
// dp[i][j]表示以当前i,j位置为终点的最短路径长度
vector<vector<int> > dp(n, vector<int>(m, 0));
dp[0][0] = matrix[0][0];
// 处理第一列
for (int i = 1; i < n; i++)
{
dp[i][0] = matrix[i][0] + dp[i - 1][0];
}
// 处理第一行
for (int j = 1; j < m; j++)
{
dp[0][j] = matrix[0][j] + dp[0][j - 1];
}
//其他按照公式来
for (int i = 1; i < n; i++)
{
for (int j = 1; j < m; j++)
{
dp[i][j] = matrix[i][j] + (dp[i - 1][j] > dp[i][j - 1] ? dp[i][j - 1] : dp[i - 1][j]);
}
}
return dp[n - 1][m - 1];
}
};
69. 把数字翻译成字符串
有一种将字母编码成数字的方式:‘a’->1, ‘b->2’, … , ‘z->26’。我们把一个字符串编码成一串数字,再考虑逆向编译成字符串。由于没有分隔符,数字编码成字母可能有多种编译结果,例如 11 既可以看做是两个 ‘a’ 也可以看做是一个 ‘k’ 。但 10 只可能是 ‘j’ ,因为 0 不能编译成任何结果。现在给一串数字,返回有多少种可能的译码结果。
思路:对于普通数字1-9,译码方式只有一种,但是对于11-19,21-26,译码方式有可选择的两种方案,因此我们使用动态规划将两种方案累计。
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// 对于普通数字1-9,译码方式只有一种,但是对于11-19,21-26,译码方式有可选择的两种方案。
class Solution {
public:
int solve(string nums)
{
int len = nums.size();
// 空数字串 以'0'开始的数字串
if (len == 0 || nums[0] == '0')
{
return 0;
}
// dp[i]用于存储到第i位数字的译码方案数
vector<int> dp(len, 0);
// 第一位数结尾的译码方法数必为1
dp[0] = 1;
for (int i = 1; i < len; i++)
{
if (nums[i] == '0')
{
if (nums[i - 1] == '1' || nums[i - 1] == '2')
{
// 数字串以10或者20开头的情况
if (i == 1)
{
dp[i] = 1;
}
// 数字串中存在10或者20,当前译码数等于后退两步的译码数
else
{
dp[i] = dp[i - 2];
}
}
}
else if (nums[i - 1] == '1' || (nums[i - 1] == '2' && nums[i] >= '1' && nums[i] <= '6'))
{
// 数字串开始不是10或者20的情况
if (i == 1)
{
dp[i] = 2;
}
else
{
dp[i] = dp[i - 1] + dp[i - 2];
}
}
else
{
dp[i] = dp[i - 1];
}
}
return dp[len - 1];
}
};
71. 最长上升子序列
给定一个长度为 n 的数组 arr,求它的最长严格上升子序列的长度。所谓子序列,指一个数组删掉一些数(也可以不删)之后,形成的新数组。例如 [1,5,3,7,3] 数组,其子序列有:[1,3,3]、[7] 等。但 [1,6]、[1,3,5] 则不是它的子序列。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int LIS(vector<int>& arr)
{
// 设置数组长度大小的动态规划辅助数组
vector<int> dp(arr.size(), 1);
int res = 0;
for (int i = 1; i < arr.size(); i++)
{
for (int j = 0; j < i; j++)
{
// 可能j不是所需要的最大的,因此需要dp[i] < dp[j] + 1
if (arr[i] > arr[j] && dp[i] < dp[j] + 1)
{
// i点比j点大,理论上dp要加1
dp[i] = dp[j] + 1;
// 找到最大长度
res = max(res, dp[i]);
}
}
}
return res;
}
};
72. 连续子数组的最大和
输入一个长度为n的整型数组array,数组中的一个或连续多个整数组成一个子数组,子数组最小长度为1。求所有子数组的和的最大值。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int FindGreatestSumOfSubArray(vector<int> array)
{
// 记录到下标i为止的最大连续子数组和
vector<int> dp(array.size(), 0);
dp[0] = array[0];
int maxsum = dp[0];
for (int i = 1; i < array.size(); i++)
{
// 状态转移:连续子数组和最大值
dp[i] = max(dp[i - 1] + array[i], array[i]);
// 维护最大值
maxsum = max(maxsum, dp[i]);
}
return maxsum;
}
};
73. 最长回文子串
对于长度为n的一个字符串A(仅包含数字,大小写英文字母),请设计一个高效算法,计算其中最长回文子串的长度。
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
class Solution {
public:
int fun(string& s, int begin, int end)
{
// 每个中心点开始扩展
while (begin >= 0 && end < s.length() && s[begin] == s[end])
{
begin--;
end++;
}
// 返回长度
return end - begin - 1;
}
int getLongestPalindrome(string A)
{
int maxlen = 1;
// 以每个点为中心
for (int i = 0; i < A.length() - 1; i++)
{
// 分奇数长度和偶数长度向两边扩展
maxlen = max(maxlen, max(fun(A, i, i), fun(A, i, i + 1)));
}
return maxlen;
}
};
74. 数字字符串转化成IP地址
现在有一个只包含数字的字符串,将该字符串转化成IP地址的形式,返回所有可能的情况。
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<string> restoreIpAddresses(string s)
{
vector<string> res;
int n = s.length();
// 遍历IP的点可能的位置
// 第一个点的位置
for (int i = 1; i < 4 && i < n - 2; i++)
{
// 第二个点的位置
for (int j = i + 1; j < i + 4 && j < n - 1; j++)
{
// 第三个点的位置
for (int k = j + 1; k < j + 4 && k < n; k++)
{
// 最后一段剩余数字不能超过3
if (n - k >= 4)
{
continue;
}
// 从点的位置分段截取
string a = s.substr(0, i);
string b = s.substr(i, j - i);
string c = s.substr(j, k - j);
string d = s.substr(k);
// IP每个数字不大于255
if (stoi(a) > 255 || stoi(b) > 255 || stoi(c) > 255 || stoi(d) > 255)
{
continue;
}
// 排除前导0的情况
if ((a.length() != 1 && a[0] == '0') || (b.length() != 1 && b[0] == '0') || (c.length() != 1 && c[0] == '0') || (d.length() != 1 && d[0] == '0'))
{
continue;
}
// 组装IP地址
string temp = a + "." + b + "." + c + "." + d;
res.push_back(temp);
}
}
}
return res;
}
};
八、字符串
83. 字符串变形
对于一个长度为 n 字符串,我们需要对它做一些变形。首先这个字符串中包含着一些空格,就像"Hello World"一样,然后我们要做的是把这个字符串中由空格隔开的单词反序,同时反转每个字符的大小写。比如"Hello World"变形后就变成了"wORLD hELLO"。
- 遍历字符串,遇到小写字母,转换成大写,遇到大写字母,转换成小写,遇到空格正常不变。
- 第一次反转整个字符串,这样基本的单词逆序就有了,但是每个单词的字符也是逆的。
- 再次遍历字符串,以每个空格为界,将每个单词反转回正常。
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
string trans(string s, int n)
{
if (n == 0)
{
return s;
}
string res;
// 大小写转换
for (int i = 0; i < n; i++)
{
if (s[i] >= 'A' && s[i] <= 'Z')
{
res += s[i] - 'A' + 'a';
}
else if (s[i] >= 'a' && s[i] <= 'z')
{
res += s[i] - 'a' + 'A';
}
// 空格直接复制
else
{
res += s[i];
}
}
// 翻转整个字符串
reverse(res.begin(), res.end());
// 以空格为界,二次翻转
for (int i = 0; i < n; i++)
{
int j = i;
while (j < n && res[j] != ' ')
{
j++;
}
reverse(res.begin() + i, res.begin() + j);
i = j;
}
return res;
}
};
84. 最长公共前缀
给你一个大小为 n 的字符串数组 strs ,其中包含n个字符串 , 编写一个函数来查找字符串数组中的最长公共前缀,返回这个公共前缀。
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
string longestCommonPrefix(vector<string>& strs)
{
int n = strs.size();
// 空字符串数组
if (n == 0)
{
return "";
}
// 以第一个字符串为基准
for (int i = 0; i < strs[0].length(); i++)
{
char temp = strs[0][i];
// 遍历后续的字符串
for (int j = 1; j < n; j++)
{
if (i == strs[j].length() || strs[j][i] != temp)
{
return strs[0].substr(0, i);
}
}
}
// 此字符串有就是最长前缀
return strs[0];
}
};
85. 验证IP地址
IPv4 地址由十进制数来表示,每个地址包含4个十进制数,其范围为 0 - 255, 用(“.”)分割。同时,IPv4 地址内的数不会以 0 开头。
IPv6 地址由8组16进制的数字来表示。这些组数字通过 ":"分割。而且,可以加入一些以 0 开头的数字,字母可以使用大写,也可以是小写。即,忽略 0 开头,忽略大小写。
然而,我们不能因为某个组的值为 0,而使用一个空的组,以至于出现(::)的情况。 比如, 2001:0db8:85a3::8A2E:0370:7334 是无效的 IPv6 地址。同时,在 IPv6 地址中,多余的 0 也是不被允许的。比如, 02001:0db8:85a3:0000:0000:8a2e:0370:7334 是无效的。
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Solution {
public:
// 字符串分割 “.”“:”
vector<string> split(string s, string spliter)
{
vector<string> res;
int i;
// 分割字符串
// 查找成功,返回按照查找规则找到的第一个字符的位置;如果查找失败,返回string::npos
while ((i = s.find(spliter)) && i != s.npos)
{
res.push_back(s.substr(0, i));
s = s.substr(i + 1);
}
res.push_back(s);
return res;
}
bool isIPv4(string IP)
{
vector<string> s = split(IP, ".");
// IPv4必定为4组
if (s.size() != 4)
{
return false;
}
for (int i = 0; i < s.size(); i++)
{
// 每组不可缺省
if (s[i].size() == 0)
{
return false;
}
// 数字位数为1-3,不能有前缀零
if (s[i].size() < 0 || s[i].size() > 3 || (s[i][0] == '0' && s[i].size() != 1))
{
return false;
}
// 每个字符必须为数字
for (int j = 0; j < s[i].size(); j++)
{
if (!isdigit(s[i][j]))
{
return false;
}
}
// 每组在0-255之间
int num = stoi(s[i]);
if (num < 0 || num > 255)
{
return false;
}
}
return true;
}
bool isIPv6(string IP)
{
vector<string> s = split(IP, ":");
// IPv6必定为8组
if (s.size() != 8)
{
return false;
}
for (int i = 0; i < s.size(); i++)
{
// 数字位数为1-4
if (s[i].size() == 0 || s[i].size() > 4)
{
return false;
}
for (int j = 0; j < s[i].size(); j++) {
// 每个字符必须为数字,a-f,A-F
if (!(isdigit(s[i][j]) || (s[i][j] >= 'a' && s[i][j] <= 'f') || (s[i][j] >= 'A' && s[i][j] <= 'F')))
{
return false;
}
}
}
return true;
}
string solve(string IP)
{
if (IP.size() == 0)
{
return "Neither";
}
if (isIPv4(IP))
{
return "IPv4";
}
else if (isIPv6(IP))
{
return "IPv6";
}
return "Neither";
}
};
// 正则表达式
#include <iostream>
#include <regex>
#include <string>
using namespace std;
class Solution {
public:
string solve(string IP)
{
// IPv4 4组 0-255 无前缀0
// 172.16.254.01
regex ipv4("(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])");
// IPv6 8组 0-9、a-f、A-F ,个数必须是1-4个
// 2001:db8:85a3:0:0:8A2E:0370:7334
regex ipv6("([0-9a-fA-F]{1,4}\\:){7}[0-9a-fA-F]{1,4}");
if (regex_match(IP, ipv4))
{
return "IPv4";
}
else if (regex_match(IP, ipv6))
{
return "IPv6";
}
else
{
return "Neither";
}
}
};
86. 大数加法
以字符串的形式读入两个数字,编写一个函数计算它们的和,以字符串形式返回。
- 若是其中一个字符串为空,直接返回另一个,不用加了。
- 交换两个字符串的位置,我们是s为较长的字符串,t为较短的字符串,结果也记录在较长的字符串中。
- 从后往前遍历字符串s,每次取出字符转数字,加上进位制,将下标转换为字符串t中从后往前相应的下标,如果下标为非负数则还需要加上字符串t中相应字符转化的数字。
- 整型除法取进位,取模算法去掉十位,将计算后的结果放入较长数组对应位置。
- 如果遍历结束,进位值还有,则需要直接在字符串s前增加一个字符‘1’。
#include <iostream>
#include <string>
using namespace std;
class Solution {
public:
string solve(string s, string t)
{
// 空字符串
if (s.empty())
{
return t;
}
if (t.empty())
{
return s;
}
// s存储较长字符串,t存储较短字符串
if (s.length() < t.length())
{
swap(s, t);
}
// 进位标志
int carry = 0;
// 从后往前遍历较长的字符串
for (int i = s.length() - 1; i >= 0; i--)
{
// 转数字 + 进位
int temp = s[i] - '0' + carry;
// 较短的字符串相应的从后往前的下标
int j = i - s.length() + t.length();
// 较短字符串还有
if (j >= 0)
{
// 转数字 + 相加
temp += t[j] - '0';
}
// 取进位
carry = temp / 10;
// 去十位
temp = temp % 10;
// 修改结果
s[i] = temp + '0';
}
// 最后的进位
if (carry == 1)
s = '1' + s;
return s;
}
};
九、双指针
87. 合并两个有序的数组【简单】
给出一个有序的整数数组 A 和有序的整数数组 B ,请将数组 B 合并到数组 A 中,变成一个有序的升序数组。保证 A 数组有足够的空间存放 B 数组的元素, A 和 B 中初始的元素数目分别为 m 和 n,A的数组空间大小为 m+n
class Solution {
public:
void merge(int A[], int m, int B[], int n)
{
// 指向数组A的结尾
int i = m - 1;
// 指向数组B的结尾
int j = n - 1;
// 指向数组A空间的结尾
int k = m + n - 1;
// 从两个数组最大的元素开始,直到某一个数组遍历完
while (i >= 0 && j >= 0)
{
// 将较大的元素放到最后
if (A[i] > B[j])
{
A[k--] = A[i--];
}
else
{
A[k--] = B[j--];
}
}
// 数组A先遍历完,数组B前半部分,添加到数组A前面
// 数组B先遍历完,数组A前半部分,不用管
if (i < 0)
{
while (j >= 0)
{
A[k--] = B[j--];
}
}
}
};
88. 判断是否为回文字符串【入门】
给定一个长度为 n 的字符串,请编写一个函数判断该字符串是否回文。如果是回文请返回true,否则返回false。字符串回文指该字符串正序与其逆序逐字符一致。
class Solution {
public:
bool judge(string str)
{
// 首指针
int left = 0;
// 尾指针
int right = str.length() - 1;
// 首尾往中间靠
while (left < right)
{
//比较前后是否相同
if (str[left] != str[right])
{
return false;
}
left++;
right--;
}
return true;
}
};
89. 合并区间【中等】
给出一组区间,请合并所有重叠的区间。请保证合并后的区间按区间起点升序排列。
- 既然要求重叠后的区间按照起点位置升序排列,我们就将所有区间按照起点位置先进行排序。使用sort函数进行排序,重载比较方式为比较interval结构的start变量。
- 排序后的第一个区间一定是起点值最小的区间,我们将其计入返回数组res,然后遍历后续区间。
- 后续遍历过程中,如果遇到起点值小于res中最后一个区间的末尾值的情况,那一定是重叠,取二者最大末尾值更新res中最后一个区间即可。
- 如果遇到起点值大于res中最后一个区间的末尾值的情况,那一定没有重叠,后续也不会有这个末尾的重叠区间了,因为后面的起点只会更大,因此可以将它加入res。
struct Interval {
int start;
int end;
Interval() : start(0), end(0) {}
Interval(int s, int e) : start(s), end(e) {}
};
class Solution {
public:
//重载比较
static bool cmp(Interval& a, Interval& b) {
return a.start < b.start;
}
vector<Interval> merge(vector<Interval>& intervals)
{
vector<Interval> res;
// 特殊情况
if (intervals.size() == 0)
{
return res;
}
// 按照区间首排序
sort(intervals.begin(), intervals.end(), cmp);
// 放入第一个区间
res.push_back(intervals[0]);
// 遍历后续区间
for (int i = 1; i < intervals.size(); i++)
{
// 区间有重叠,更新结尾
if (intervals[i].start <= res.back().end)
{
res.back().end = max(res.back().end, intervals[i].end);
}
// 区间没有重叠,直接加入
else
{
res.push_back(intervals[i]);
}
}
return res;
}
};
90. 最小覆盖子串【困难】
给出两个字符串 s 和 t,要求在 s 中找出最短的包含 t 中所有字符的连续子串。题目保证字符串 s 和 t 中仅包含大小写英文字母,满足条件的子串可能有很多,但是题目保证满足条件的最短的子串唯一。如果 s 中没有包含 t 中所有字符的子串,返回空字符串。
// 哈希表 + 滑动窗口
class Solution {
public:
// 检查哈希表中是否有小于0的
bool check(unordered_map<char, int>& hash)
{
for (auto iter = hash.begin(); iter != hash.end(); iter++)
{
if (iter->second < 0)
{
return false;
}
}
return true;
}
string minWindow(string S, string T)
{
int cnt = S.length() + 1;
// 字符串T 哈希表
unordered_map<char, int> hash;
for (int i = 0; i < T.length(); i++)
{
hash[T[i]] -= 1;
}
// 滑动窗口
int slow = 0, fast = 0;
//记录左右区间
int left = -1, right = -1;
for (; fast < S.length(); fast++)
{
// 目标字符
char c = S[fast];
// 哈希表中存在
if (hash.count(c))
{
hash[c]++;
}
// 没有小于0的说明都覆盖了
while (check(hash))
{
int res = fast - slow + 1;
// 取最优解 <=
if (res <= cnt)
{
cnt = res;
left = slow;
right = fast;
}
// 目标字符
char c = S[slow];
if (hash.count(c))
{
hash[c]--;
}
//窗口缩小
slow++;
}
}
//找不到的情况
if (left == -1)
{
return "";
}
return S.substr(left, right - left + 1);
}
};
91. 反转字符串【入门】
写出一个程序,接受一个字符串,然后输出该字符串反转后的字符串。(字符串长度不超过1000)
class Solution {
public:
string solve(string str) {
// 左右双指针
int left = 0;
int right = str.length() - 1;
// 两指针往中间靠
while (left < right)
{
// 交换两边字符
swap(str[left], str[right]);
left++;
right--;
}
return str;
}
};
92. 最长无重复子数组【中等】
给定一个长度为n的数组arr,返回arr的最长无重复元素子数组的长度,无重复指的是所有数字都不相同。子数组是连续的,比如[1,3,5,7,9]的子数组有[1,3],[3,5,7]等等,但是[1,3,7]不是子数组。
- 构建一个哈希表,用于统计数组元素出现的次数。
- 窗口左右界都从数组首部开始,每次窗口优先右移右界,并统计进入窗口的元素的出现频率。
- 一旦右界元素出现频率大于1,就需要右移左界直到窗口内不再重复,将左边的元素移除窗口的时候同时需要将它在哈希表中的频率减1,保证哈希表中的频率都是窗口内的频率。
- 每轮循环,维护窗口长度最大值。
// 哈希表 + 滑动窗口
class Solution {
public:
int maxLength(vector<int>& arr)
{
// 哈希表 记录窗口内非重复的数字
unordered_map<int, int> mp;
// 窗口大小
int res = 0;
for (int left = 0, right = 0; right < arr.size(); right++)
{
// 窗口右移 哈希表统计出现次数
mp[arr[right]]++;
// 窗口内有重复
while (mp[arr[right]] > 1)
{
// 窗口左移 同时减去哈希表中该数字的出现次数
mp[arr[left++]]--;
}
// 维护子数组长度最大值
res = max(res, right - left + 1);
}
return res;
}
};
93. 盛水最多的容器【中等】
给定一个数组height,长度为n,每个数代表坐标轴中的一个点的高度,height[i]是在第i点的高度,请问,从中选2个高度与x轴组成的容器最多能容纳多少水。当n小于2时,视为不能形成容器,请返回0。
// 双指针 + 贪心算法
class Solution {
public:
int maxArea(vector<int>& height)
{
// 不能形成容器
if (height.size() < 2)
{
return 0;
}
// 容积最大值
int res = 0;
// 双指针左右界
int left = 0;
int right = height.size() - 1;
// 共同遍历完所有的数组
while (left < right)
{
// 计算区域水容量
int capacity = min(height[left], height[right]) * (right - left);
// 维护最大值
res = max(res, capacity);
//优先舍弃较短的边
if (height[left] < height[right])
{
left++;
}
else
{
right--;
}
}
return res;
}
};
94. 接雨水问题
给定一个整形数组 arr,已知其中所有的值都是非负的,将这个数组看作一个柱子高度图,计算按此排列的柱子,下雨之后能接多少雨水,数组以外的区域高度视为0。
class Solution {
public:
long long maxWater(vector<int>& arr)
{
// 空数组
if (arr.size() == 0)
{
return 0;
}
// 左右双指针
int left = 0;
int right = arr.size() - 1;
// 中间区域的边界高度
int maxL = 0;
int maxR = 0;
long long res = 0;
while (left < right)
{
// 每次维护往中间的最大边界
maxL = max(maxL, arr[left]);
maxR = max(maxR, arr[right]);
// 较短的边界确定该格子的水量
if (maxR > maxL)
{
res += maxL - arr[left++];
}
else
{
res += maxR - arr[right--];
}
}
return res;
}
};
十、贪心算法
95. 分糖果问题
一群孩子做游戏,现在请你根据游戏得分来发糖果,要求如下:
1.每个孩子不管得分多少,起码分到一个糖果。
2.任意两个相邻的孩子之间,得分较多的孩子必须拿多一些糖果。(若相同则无此限制)
给定一个数组 arrarr 代表得分数组,请返回最少需要多少糖果。
- 使用一个辅助数组记录每个位置的孩子分到的糖果,全部初始化为1。
- 从左到右遍历数组,如果右边元素比相邻左边元素大,意味着在递增,糖果数就是前一个加1,否则保持1不变。
- 从右到左遍历数组,如果左边元素比相邻右边元素大, 意味着在原数组中是递减部分,如果左边在上一轮中分到的糖果数更小,则更新为右边的糖果数+1,否则保持不变。
- 将辅助数组中的元素累加求和。
class Solution {
public:
int candy(vector<int>& arr)
{
// 每个位置的糖果数初始化为1
vector<int> nums(arr.size(), 1);
// 从左到右遍历
for (int i = 1; i < arr.size(); i++)
{
// 如果分数递增,每次增加一个
if (arr[i] > arr[i - 1])
{
nums[i] = nums[i - 1] + 1;
}
}
// 记录总糖果数
int res = nums[arr.size() - 1];
// 从右到左遍历
for (int i = arr.size() - 2; i >= 0; i--)
{
// 如果左边分数更高,但是糖果数更小
if (arr[i] > arr[i + 1] && nums[i] <= nums[i + 1])
{
nums[i] = nums[i + 1] + 1;
}
// 累加和
res += nums[i];
}
return res;
}
};
96. 主持人调度
- 利用辅助数组获取单独各个活动开始的时间和结束时间,然后分别开始时间和结束时间进行排序,方便后面判断是否相交。
- 遍历 n 个活动,如果某个活动开始的时间大于之前活动结束的时候,当前主持人就够了,活动结束时间往后一个。
- 若是出现之前活动结束时间晚于当前活动开始时间的,则需要增加主持人。
class Solution {
public:
int minmumNumberOfHost(int n, vector<vector<int> >& startEnd)
{
// 活动开始时间
vector<int> start;
// 活动结束时间
vector<int> end;
for (int i = 0; i < n; i++)
{
start.push_back(startEnd[i][0]);
end.push_back(startEnd[i][1]);
}
// 分别对开始和结束时间排序
sort(start.begin(), start.end());
sort(end.begin(), end.end());
// 主持人数
int res = 0;
int j = 0;
for (int i = 0; i < n; i++)
{
// 新开始的节目大于上一轮结束的时间,主持人不变
if (start[i] >= end[j])
{
j++;
}
else
{
// 主持人增加
res++;
}
}
return res;
}
};