链表
1.1 移除链表元素
给你一个链表的头节点
head
和一个整数val
,请你删除链表中所有满足Node.val == val
的节点,并返回 新的头节点 。
法一:直接使用原来的链表来进行移除节点操作
//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* removeElements(ListNode* head, int val) {
//删除头结点
//使用while删除头结点:是因为删除一个头结点后,新的头结点可能也需要删除
while (head != NULL && head->val == val) {//头结点不为空,且位于头结点位置的元素需删除
ListNode *tmp = head;//使用临时变量暂时存储要删除的节点
head = head->next;//旧头结点的下一个节点作为新的头结点
delete tmp;//将已删除的废弃节点释放掉,也就是手动清理节点内存
}
//删除非头结点
ListNode* cur = head;
while (cur != NULL && cur->next != NULL) {//当前节点不为空,且当前节点的下一个节点也不为空
if (cur->next->val == val) {//当前节点的下一个节点需移除
ListNode* tmp = cur->next;//使用临时变量暂时存储要删除的节点
cur->next = cur->next->next;//使当前节点的next指向下下个节点
delete tmp;//将已删除的废弃节点释放掉,也就是手动清理节点内存
}
else {
cur = cur->next;//当前节点无需移除,则开始检查下一节点
}
}
return head;
}
};
法二:设置一个虚拟头结点再进行移除节点操作
//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* removeElements(ListNode* head, int val) {
ListNode* dummyHead = new ListNode(0);//设置一个虚拟头结点
dummyHead->next = head;//将虚拟头结点指向head,以便后面做删除操作
ListNode* cur = dummyHead;
while (cur->next != NULL) {//当前节点的下一个节点不为空
if (cur->next->val == val) {//当前节点的下一个节点需移除
ListNode* tmp = cur->next;//使用临时变量暂时存储要移除的节点
cur->next = cur->next->next;//使当前节点的next指向下下个节点
delete tmp;//将已删除的废弃节点释放掉,也就是手动清理节点内存
}
else {
cur = cur->next;//当前节点无需移除,则开始检查下一节点是否需要移除
}
}
head = dummyHead->next;//虚拟头结点所指向的下一节点才是真正头结点
delete dummyHead;//将虚拟头结点释放掉,即手动清理虚拟头结点所占用的内存
return head;//返回真正头结点
}
};
1.2 设计链表
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:
val
和next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。如果是双向链表,则还需要属性
prev
以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
class MyLinkedList {
public:
struct LinkedNode {//定义链表节点的结构体
int val;
LinkedNode* next;
LinkNode() :val(0), next(nullptr) {}
LinkedNode(int val) :val(val), next(nullptr){}
LinkNode(int x, LinkNode* next) :val(x), next(next) {}
};
MyLinkedList() {//构造函数,初始化链表
m_size = 0;
m_dummyHead = new LinkedNode(0);//虚拟头结点,并非真正头结点
}
//获取下标为index的节点数值,若index非法则返回-1
//注意:index从0开始,第0个节点即为头结点
int get(int index) {
if (index < 0 || index >= m_size) return -1;
LinkedNode* cur = m_dummyHead;
for (int i = 0; i <= index; i++) {
cur = cur->next;
}//循环结束出来时,cur指向的是下标为index的节点
return cur->val;
/*
或
while (index--) {
cur = cur->next;
}
return cur->next->val;
*/
}
//在链表头部添加新节点,即插入新的头结点
void addAtHead(int val) {
LinkedNode* tmp = new LinkedNode(val);
tmp->next = m_dummyHead->next;
m_dummyHead->next = tmp;
m_size++;
}
//在链表尾部添加新节点
void addAtTail(int val) {
LinkedNode* tmp = new LinkedNode(val);
LinkedNode* cur = m_dummyHead;
for (int i = 0; i < m_size; i++) {
cur = cur->next;
}//在循环结束出来时,cur指向的是链表的尾部节点
cur->next = tmp;
m_size++;
/*
或
while (cur->next != nullptr) {
cur = cur->next;
}
cur->next = tmp;
m_size++;
*/
}
//在链表下标为index处的前面添加新节点,若index=0,则为添加新头结点
//若index=链表的长度,则为添加链表的尾结点
//若index>链表的长度,则为无效下标,返回空
//若index<0,则为添加新的头结点
void addAtIndex(int index, int val) {
LinkedNode* tmp = new LinkedNode(val);
if (index > m_size) return;
if (index < 0) index = 0;
LinkedNode* cur = m_dummyHead;
while (index--) {
cur = cur->next;
}//在循环结束时,cur指向的是下标为index位置的前一个节点
tmp->next = cur->next;
cur->next = tmp;
m_size++;
}
//删除下标为index的节点,若index>链表长度,则直接return。(index为从0开始)
void deleteAtIndex(int index) {
if (index < 0 || index >= m_size) return;
LinkedNode* cur = m_dummyHead;
while (index--) {
cur = cur->next;
}//在循环结束时,cur指向的是下标为index位置的前一个节点
LinkedNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
//delete命令只是释放了tmp指针原来所指的那部分内存
//被delete后的指针tmp的值(地址)并非就是NULL,而是随机值。
//也就是被delete后,若不加上一句 tmp = nullptr; tmp就会成为乱指的野指针
//若之后的程序不小心使用了tmp,则会指向难以预想的内存空间
tmp = nullptr;
m_size--;
}
//打印链表
void printLinkedList() {
LinkedNode *cur = m_dummyHead;
while (cur->next != nullptr) {
cout << cur->next->val << " ";
cur = cur->next;
}
}
private:
int m_size;
LinkedNode* m_dummyHead;
};
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList* obj = new MyLinkedList();
* int param_1 = obj->get(index);
* obj->addAtHead(val);
* obj->addAtTail(val);
* obj->addAtIndex(index,val);
* obj->deleteAtIndex(index);
*/
1.3 反转链表(可双指针)
给你单链表的头节点
head
,请你反转链表,并返回反转后的链表。
双指针法:(从前往后翻转指针指向)
//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* reverseList(ListNode* head) {
//双指针法
ListNode* tmp;
ListNode* cur = head;
ListNode* pre = nullptr;
while (cur != nullptr) {
tmp = cur->next;//提前备份cur的下一个节点,因为下面要改变cur->next了
cur->next = pre;//将pre和cur指针之间指向做翻转操作
pre = cur;//更新 pre 和 cur 指针
cur = tmp;
}
return pre;//返回新的头结点
}
};
递归法1:代码逻辑与双指针法一样 (从前往后翻转指针指向)
//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* reverseList(ListNode* head) {
//递归法1:代码逻辑与双指针法一样
return reverse(nullptr, head);//初始在头部,pre指向空,cur指向头
}
ListNode* reverse(ListNode* pre, ListNode*cur) {
if (cur == nullptr) return pre;//cur指向空了,则代表pre到圆链表的尾部了
ListNode* tmp = cur->next;//备份cur->next,因为后面做翻转时若不备份则会丢失
cur->next = pre;//将相邻两个指针之间的指向性做翻转操作
return reverse(cur, tmp);//移动pre和cur指针使其向后走
}
};
递归法2:(从后往前翻转指针指向)
//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* reverseList(ListNode* head) {
//递归法2:从后往前翻转
//传进来的形参head相当于当前位置cur,一开始为原链表的头
//若本来链表就是空则直接返回空,或当前位置走到原链表尾部next则开始返回空
if (head == nullptr) return nullptr;
//若本来链表就只有一个节点则直接返回该节点,或当前位置走到原链表尾部则开始返回该尾部
if (head->next == nullptr) return head;
//此处往前的部分都是【从前往后】一直走,走到链表末尾处
ListNode* last = reverseList(head->next);
//此处往后的部分都是【从后往前】一直走,走到链表头结点处
//head相当于当前位置cur,head->next相当于当前位置的下一个
//cur与其后一个节点之间的指向做翻转操作
head->next->next = head;
//做完翻转操作后head->next相当于(cur,pre)中的cur,head就是pre,因此pre的指向要往前转
//又因最后会是新链表的尾节点,所以就让其指向空
head->next = nullptr;
return last;//last记录的是原链表的尾部节点,也就是新链表的头结点
}
};
递归法2:(从后往前翻转指针指向) 更易懂版本
//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* reverseList(ListNode* head) {
return reverse(head);
}
ListNode* reverse(ListNode* cur) {
if (cur == nullptr) return nullptr;
if (cur->next == nullptr) return cur;
ListNode* last = reverse(cur->next);
cur->next->next = cur;
cur->next = nullptr;
return last;
}
};
1.4 成对交换节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
//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* swapPairs(ListNode* head) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* cur = dummyHead;
/*
cur初始为指向链表头结点的虚拟头结点dummyHead
原链表结构: A->B->C->D
加了虚拟头结点后: dummyHead->A->B->C->D
用了cur表示后: cur->A->B->C->D
更改后的新链表结构: B->A->D->C
逻辑步骤:(cur->A->B->C->D)
1、使cur指向B
2、使 B 指向A
3、使 A 指向C
注:
由于执行1时会导致丢失A,因此需先用tmp1备份A;
同理,执行2时会导致丢失C,因此需先用tmp2备份C;
所有指向操作完成后,使cur往后走两个节点,继续重复此步骤,直至链表尾部结束
*/
while (cur->next != nullptr && cur->next->next != nullptr) {
ListNode* tmp1 = cur->next;//备份A
ListNode* tmp2 = cur->next->next->next;//备份C
cur->next = cur->next->next;//使cur指向B
cur->next->next = tmp1;//使B指向A(此时cur->next已是B)
tmp1->next = tmp2;//使A指向C
cur = cur->next->next;//cur往后走两个节点
}
return dummyHead->next;
}
};
1.5 删除链表的倒数第 N 个结点(快慢指针)
给你一个链表,删除链表的倒数第
n
个结点,并且返回链表的头结点。
//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* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;//虚拟头结点指向头
ListNode* fast = dummyHead;
ListNode* slow = dummyHead;
while (n-- && fast->next != nullptr) {
fast = fast->next;//快指针先走n步
}
fast = fast->next;//快指针走多一步使后面慢指针少走一步,以便慢指针停止要删除的节点的前一个位置
while (fast != nullptr) {
fast = fast->next;//快慢指针一起走直到快指针走到链表尾部的后一个位置
slow = slow->next;
}
ListNode* tmp = slow->next;//备份已删除的节点
slow->next = slow->next->next;//删除节点
delete tmp;//释放所删除节点的内存
return dummyHead->next;
}
};
1.6 两个链表的交集 LCCI
给你两个单链表的头节点
headA
和headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回null
。
//Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* curA = headA;
ListNode* curB = headB;
int lenA = 0, lenB = 0;
while (curA != nullptr) {
curA = curA->next;
lenA++;
}//获取A链表的长度
while (curB != nullptr) {
curB = curB->next;
lenB++;
}//获取B链表的长度
int gap = abs(lenA - lenB);//两个链表的长度差
curA = headA;
curB = headB;
//使A始终为较长的那个链表,B为较短的那个链表
if (lenA < lenB) {
swap(lenA, lenB);
swap(curA, curB);
}
//较长的那个链表先走一段,使两个链表后面走的距离相同(同步走),
//以便后续比较是否碰到相同的节点,即两个链表的相交处
while (gap--) {
curA = curA->next;
}
//链表相交处后的重叠部分长度一定相同
//此时开始两个链表同步走,遇到相同节点则代表找到链表相交处
while (curA != nullptr && curB != nullptr) {
if (curA == curB) return curA;
curA = curA->next;
curB = curB->next;
}
return NULL;
}
};
1.7 环形链表 II(快慢指针)
给定一个链表的头节点
head
,返回链表开始入环的第一个节点。 如果链表无环,则返回null
。如果链表中有某个节点,可以通过连续跟踪
next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果pos
是-1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。不允许修改 链表。
思路如下:
/*
快慢指针:
快指针每次走两步,
慢指针每次走一步。
假设:
链表头结点到环入口(入环点)的距离为 a
环入口到相遇节点的距离为 b
环的长度为 c
则:
快指针所走距离: f=a+b+c;
慢指针所走距离: s=a+b;
已知快指针的速度使慢指针的两倍,相同时间情况下,快指针所走距离=慢指针所走距离的两倍。
即: f=2s
所以有: a+b+c = 2(a+b)
得: c=a+b ===> a=c-b (a为链表头到入环口的距离)
即:链表头到入环口的距离(所求)=环内相遇节点位置到入环口的距离
结论:
得到有环且环内相遇节点后,一个指针留在相遇节点,一个指针回到链表头节点,
两个指针同时出发,一次走一步,再次相遇处,则为入环口节点位置。
*/
//Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while (fast != nullptr && fast->next != nullptr) {
fast = fast->next->next;//快指针每次走两格
slow = slow->next;//慢指针每次走一格
if (fast == slow) {//当快慢指针相遇了,则代表链表有环,可以开始找入环点了
fast = head;//此时两个指针都位于环内的相遇节点处,使其中一个指针回到链表起点
while (fast != slow) {
fast = fast->next;//此时一个指针在相遇处,一个指针在链表起点处
slow = slow->next;//两个指针同时出发,每次走一步
}
return fast;//重新相遇处则为链表入环点
}
}
//其中任何一个指针走到nullptr了,则代表链表无环,直接返回null
return NULL;
}
};
哈希表
哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表.
一般哈希表都是用来快速判断一个元素是否出现集合里。
遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。(第一时间)
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
即:一个元素【是否出现过】【是否在集合里】。
初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数。
哈希函数 是把传入的key 映射到 符号表的索引上。
哈希碰撞 处理有多个key 映射到 相同索引上时的情景,处理碰撞的普遍方式是拉链法和线性探测法。
常见的三种哈希结构
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
- 数组
- set (集合)
- map (映射)
set(集合):
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
map(映射):
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
map中:
第一个为关键字(每个关键字只能在 map 中出现一次);
第二个为关键字的值。
查找元素是否出现过:
1).find() 方法返回值是一个迭代器,成功返回迭代器指向要查找的元素,失败返回的迭代器指向end。
.find() == .end() 则代表未出现过;
.find() != .end() 则代表已出现过;
2).count() 方法返回值是一个整数,1表示有这个元素,0表示没有这个元素。
.count()==0 则代表未出现过;
.count()==1 则代表已出现过;
哈希表经典题目 解题方式:
1)数组作为哈希表
使用map的空间消耗要比数组大一些,因为map要维护红黑树或者符号表,而且还要做哈希函数的运算。所以允许的情况下,数组更加简单直接有效!
2)set 作为哈希表
- 数组的大小是有限的,受到系统栈空间(不是数据结构的栈)的限制。
- 如果数组空间够大,但哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
所以此时一样的做映射的话,就可以使用set了。
3)map 作为哈希表
- 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
- set是一个集合,里面放的元素只能是一个key,而 1.4 两数之和 这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以 set 也不能用。
map是一种
<key, value>
的结构,本题可以用key保存数值,用value在保存数值所在的下标。所以使用map最为合适。
【注】优化建议:
使用unordered_map代替map;
使用++i(代指)代替i++;
使用-c-b代替-(c+b);
使用+,-,*,/代替+=等运算;
以上四条建议可以提升速度,减小内存消耗
引入头文件:
#include <unordered_set>
#include <unordered_map>
1.1 有效的字母异位词
给定两个字符串
s
和t
,编写一个函数来判断t
是否是s
的字母异位词。注意:若
s
和t
中每个字符出现的次数都相同,则称s
和t
互为字母异位词。
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = { 0 };//总共有26个字母,每个元素为对应字母的出现个数,先全部初始化为0
for (int i = 0; i < s.size(); i++) {
record[s[i] - 'a']++;//记录字符串s中每个字母的出现个数
}
for (int i = 0; i < t.size(); i++) {
record[t[i] - 'a']--;//在已记录完s中每个字母出现个数的前提下,对t中出现的字符对应数量-1
}
for (int i = 0; i < 26; i++) {
//record数组若有元素不为0,则证明字符串s或t其中一个多了字符或少了字符,可以直接返回false了
if (record[i] != 0) return false;
}
//数组元素全0则代表两字符串长度相等且字母出现数量相等
return true;
}
};
1.2 两个数组的交集
给定两个数组
nums1
和nums2
,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set;//存放结果,用set是为了给结果集去重
unordered_set<int> nums_set(nums1.begin(), nums1.end());
for (int num : nums2) {
//遍历nums2中的元素,若在nums1中也出现过,即nums_set中出现过
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
1.3 快乐数
编写一个算法来判断一个数
n
是不是快乐数。「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。
如果
n
是 快乐数 就返回true
;不是,则返回false
。
哈希表版:
class Solution {
public:
int getSum(int n) {
int sum = 0;
while (n) {
//每次只处理个位上的数值
sum += (n % 10)*(n % 10);
n /= 10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> sum_set;//用set存sum是为了去重
while (1) {
int sum = getSum(n);
if (sum == 1) return true;//快乐数
//若之前已碰到过该sum,则证明已进入循环,可以直接false了
if (sum_set.find(sum) != sum_set.end()) {
return false;
}
else {//之前未碰到该sum,则可记录
sum_set.insert(sum);
}
n = sum;
}
}
};
快慢指针版:
快指针速度 是 慢指针速度的两倍,只要有循环,快慢指针总会相遇。在相遇时,判断是否是因为1引起的循环。是则快乐数,否则非。
在 int 数据范围中,哪一个数字 n 它所对应的下一个数字是最大的?
我们能构造得到 1 999 999 999,那么只有构造出1个1,9个9的数字在int范围内就是最大的,
那么下一个节点时多少呢?
根据快乐数的定义能得到 9^2 x 9 + 1 = 730 ,
那么就是在整形范围之内任何一个数字所能映射到的下一个数字都不会超过 730,
也就意味着当前所抽象出来的链表结构中节点数目最多不会超过 730 个。
如果快指针一次走两步、慢指针一次走一步的话,
那么慢指针走的最多,也只不过走了 731 x 2 = 1462步,而快指针就是走了 731 步。所以至此就证明完了,在整形快乐数中进行单链表判环的操作的话,操作步骤是有限的。
就取个整吧,最多最多也就 2000 步了。所以快慢指针这个方案是高效可行的。
class Solution {
public:
int getSum(int n) {
int sum = 0;
while (n) {
sum += (n % 10)*(n % 10);
n /= 10;
}
return sum;
}
bool isHappy(int n) {
int fast = n, slow = n;
do {
slow = getSum(slow);
fast = getSum(fast);
fast = getSum(fast);
} while (slow != fast);
//while (1) {
// slow = getSum(slow);
// fast = getSum(fast);
// fast = getSum(fast);
// if (slow == fast) break;
//}
if (slow == 1) return true;
return false;
}
};
1.4 两数之和
给定一个整数数组
nums
和一个整数目标值target
,请你在该数组中找出 和为目标值target
的那 两个 整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
思路:
不仅要知道元素是否遍历过,还要知道这个元素对应的下标,需要使用 key value 结构来存放,key来存元素,value来存下标,那么使用map正合适。
题目中并不需要key有序,选择std::unordered_map 效率更高!
判断元素是否出现,这个元素就要作为key ==》数组中的元素作为key;
key对应的就是value,value用来存下标。
即:map中的存储结构为 {key:数据元素,value:数组元素对应的下标}。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> result_map;
for (int i = 0; i < nums.size(); i++) {
//遍历当前元素,并在map中查询是否有匹配的key
auto iter = result_map.find(target - nums[i]);
if (iter != result_map.end()) {
return vector<int>{iter->second, i};
}
//未查询到匹配对,则将访问过的元素及其对应下标存入
result_map.insert(pair<int, int>(nums[i], i));
}
return {};
}
};
1.5 四数相加 II
给你四个整数数组
nums1
、nums2
、nums3
和nums4
,数组长度都是n
,请你计算有多少个元组(i, j, k, l)
能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
该题是使用哈希法经典题目。而 三数之和, 四数之和并不适用哈希法,因为三数之和和四数之和这两道题目使用哈希法在不超时的情况下做到对结果去重是很困难的,很有多细节需要处理。
这该题目是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况,所以相对于题目18. 四数之和,题目15.三数之和,还是简单了不少。
解题思路:
将问题规模缩小,将A,B放在一起考虑,将C,D放在一起考虑。
这样划分过后,问题就变成了:在C,D的元素和中找A,B元素和的相反数。
先用map存A,B的元素和(key为元素和,value为元素和出现的个数)。
再在C,D中查找A,B元素和的相反数。
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
int count = 0;
//key:a+b的和
//value:a+b的和 出现的次数
unordered_map<int, int> sum12_map;
for (int i : nums1) {
for (int j : nums2) {
++sum12_map[i + j];//++放前面只是为了减小内存消耗
}
}
//遍历C,D数组,若找到 0-(c+d) 在map中出现过的话,
//则其在map中记录的对应的出现次数可加入count的统计
for (int i : nums3) {
for (int j : nums4) {
if (sum12_map.find(-i - j) != sum12_map.end())
count = count + sum12_map[-i - j];
}
}
return count;
}
};
1.6 赎金信
给你两个字符串:
ransomNote
和magazine
,判断ransomNote
能不能由magazine
里面的字符构成。如果可以,返回
true
;否则返回false
。
magazine
中的每个字符只能在ransomNote
中使用一次。
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
vector<int> record(26, 0);
//半身size就不够,则可直接提前返回false了
if (magazine.size() < ransomNote.size()) return false;
//先记录已有可进行的字符串(法一)
for (char c : magazine) ++record[c - 'a'];//记录备选字符数量
for (char c : ransomNote) {
--record[c - 'a'];//需消除的字符 使备选字符中对应的数量-1
if (record[c - 'a'] < 0) return false;//一旦有数量不够消除的可直接返回false
}
先记录要进行消除的字符串(法二)
//for (char c : ransomNote) {
// ++record[c - 'a'];//先记录ransomNote中各个字符出现的次数
//}
//for (char c : magazine) {//对ransomNote中的字符做消除
// --record[c - 'a'];
//}
//for (int i : record) {//只要有一个没有消除掉,余额>0则可直接false
// if (i > 0) return false;
//}
return true;
}
};
1.7 三数之和
与 双指针法/快慢指针法 1.5 三数之和 同。
双指针法/快慢指针法
判断循环就可以用快慢指针。
如带环链表找入口的题,也是一个slow,一个fast。fast速度是slow的两倍。 这样只要有循环,两者总会相遇。(快慢指针相遇时,即为一个循环周期)
1.1 删除元素
给你一个数组
nums
和一个值val
,你需要 原地 移除所有数值等于val
的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 额外空间并 原地 修改输入数组。
O(1)
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
法一:不改变元素的相对位置
双指针法(快慢指针法):
通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
定义快慢指针
- 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组(用于遍历)
- 慢指针:指向更新 新数组下标的位置(用于指向 存放满足要求的元素 的下标位置)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slowIndex = 0;
//slowIndex为nums中值不为val的元素个数,将nums中前slowIndex个位置都摆放为 不为val的元素
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (nums[fastIndex] != val) {
nums[slowIndex++] = nums[fastIndex];
}
}
return slowIndex;
}
};
法二:改变元素了的相对位置,但确保了移动最少元素
相向双指针方法:
基于元素顺序可以改变的题目描述改变了元素相对位置,确保了移动最少元素
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int leftIndex = 0;
int rightIndex = nums.size() - 1;
while (leftIndex <= rightIndex) {
//找到左边值为val的元素
while (leftIndex <= rightIndex && nums[leftIndex] != val) {
leftIndex++;
}
//找到右边不为val的元素
while (leftIndex <= rightIndex && nums[rightIndex] == val) {
rightIndex--;
}
//用右边不为val的元素直接覆盖掉左边值为val的元素
if (leftIndex < rightIndex) {
nums[leftIndex++] = nums[rightIndex--];
}
}
//此时已将左边值为val的元素全部更替为右边不为val的元素
//leftIndex一定是指向最终数组的下一个元素,即值其值正好就为最终数组的长度size
return leftIndex;
}
};
1.2 有序数组的平方
给你一个按 非递减顺序 排序的整数数组
nums
,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
/*
数组其实是有序的, 只不过负数平方之后可能成为最大数了。
那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。
因此可以考虑双指针法了,一个指针指向起始位置,一个指针指向终止位置
*/
vector<int> result(nums.size());
int k = result.size() - 1;//初始指向结果数组的尾部,也就是存放数值最大元素的位置
int leftIndex = 0;
int rightIndex = nums.size() - 1;
while (leftIndex <= rightIndex) {
if (nums[leftIndex] * nums[leftIndex] > nums[rightIndex] * nums[rightIndex]) {
result[k] = nums[leftIndex] * nums[leftIndex];
leftIndex++;
}
else {
result[k] = nums[rightIndex] * nums[rightIndex];
rightIndex--;
}
k--;
}
return result;
}
};
或从大到小存进结果组后再翻转数组:
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
vector<int> result;
int leftIndex = 0;
int rightIndex = nums.size() - 1;
while (leftIndex <= rightIndex) {
int a = nums[leftIndex] * nums[leftIndex];
int b = nums[rightIndex] * nums[rightIndex];
if (a > b) {
result.push_back(a);
leftIndex++;
}
else {
result.push_back(b);
rightIndex--;
}
}
reverse(result.begin(), result.end());
return result;
}
};
1.3 替换空格
请实现一个函数,把字符串
s
中的每个空格替换成"%20"。
class Solution {
public:
string replaceSpace(string s) {
int count = 0;//空格数量
for(int i = 0; i < s.size(); i++) {
if (s[i] == ' ') count++;
}
int oldSize = s.size();//旧字符串的长度
s.resize(s.size() + count * 2);//扩充字符串s的大小,使其增长到刚好将空格替换成%20后的长度
int newSize = s.size();//新字符串的长度
for (int i = oldSize - 1, j = newSize - 1; i < j; i--, j--) {
if (s[i] == ' ') {//碰到空格部分
s[j] = '0';
s[--j] = '2';//替换填入发同时将下标j往前移
s[--j] = '%';
}
else {//非空格部分直接填入
s[j] = s[i];
}
}
return s;
}
};
1.4 反转字符串中的单词
给你一个字符串
s
,请你反转字符串中 单词 的顺序。单词 是由非空格字符组成的字符串。
s
中使用至少一个空格将字符串中的 单词 分隔开。返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
注意:输入字符串
s
中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
解题思路如下:
- 移除多余空格
- 将整个字符串反转
- 将每个单词反转
举个例子,源字符串为:"the sky is blue "
- 移除多余空格 : "the sky is blue"
- 字符串反转:"eulb si yks eht"
- 单词反转:"blue is sky the"
这样就能完成翻转字符串里的单词。
class Solution {
public:
// 反转字符串s中左闭右闭的区间 [begin, end]
void reverse(string& s, int begin, int end) {
for (int i = begin, j = end; i < j; i++, j--) {
if (s[i] != s[j]) {
char tmp = s[i];
s[i] = s[j];
s[j] = tmp;
}
}
}
string reverseWords(string s) {
int slow = 0;//慢指针,存放删除空格后的元素下标位置
for (int i = 0; i < s.size(); i++) {//第一步:移除字符串中多余的空格
if (s[i] == ' ') continue;//只对非空格部分做处理,空格跳过(删除)
if (slow != 0) s[slow++] = ' ';//慢指针位于0则代表第一个单词仍未存入,所以不加空格
while (i < s.size() && s[i] != ' ') {
s[slow++] = s[i++];//将空格前的单词完整存入
}
}
//因为是先存入再++,所以slow就是最后存入元素后一位的下标,即可代表删除空格后的字符串长度
//第二步:反转整个字符串
s.resize(slow);
//此时已将冗余的空格全部移除,可以开始做字符串反转
reverse(s, 0, s.size() - 1);//先做字符串整体反转
int start = 0;
for (int i = 0; i < s.size(); i++) {//第三步:逐个将每个单词反转
if (s[i] == ' ' || i == s.size() - 1) {//碰到空格或字符串结尾代表一个单词
if (i == s.size() - 1) i++;
reverse(s, start, i - 1);//再将单个单词反转回原来的正确排序
start = i + 1;
}
}
return s;
}
};
1.5 三数之和
给你一个整数数组
nums
,判断是否存在三元组[nums[i], nums[j], nums[k]]
满足i != j
、i != k
且j != k
,同时还满足nums[i] + nums[j] + nums[k] == 0
。请你返回所有和为0
且不重复的三元组。注意:答案中不可以包含重复的三元组。
思路:
要求 a+b+c = 0
即 nums[i] + nums[left] + nums[right] = 0
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());//需要先对数组排序,从小到大排,以便去重
for (int i = 0; i < nums.size(); i++) {
//第一个元素数值就已经>0,证明不可能为结果要的三元组,可以直接返回了(因为nums已是从小到大排好序的)
if (nums[i] > 0) break;
//可选项的前三个元素数值和已>0,后面的任何组合之和都不可能比该值小,所以也可以直接返回了
if (i + 2 < nums.size() && nums[i] + nums[i + 1] + nums[i + 2] > 0) break;
//必须是当前数与前一个数比较是否相等
//若是与当前数的后一个数比较的话会将当前数的组合也一起漏掉,不可以漏掉
if (i > 0 && nums[i] == nums[i - 1]) continue;//第一个数去重
int left = i + 1;
int right = nums.size() - 1;
//要求 a+b+c = 0
//即 nums[i] + nums[left] + nums[right] = 0
while (left < right) {
if (nums[i] + nums[left] + nums[right] < 0) {
left++;//三数之和小了,则需left向右走一步
}
else if (nums[i] + nums[left] + nums[right] > 0) {
right--;//三数之和大了,则需right像左走一步
}
else {//满足要求 a+b+c = 0
result.push_back(vector<int>{ nums[i], nums[left], nums[right] });
while (left < right&&nums[left] == nums[left + 1]) left++;//第二个数去重
while (left < right&&nums[right] == nums[right - 1]) right--;//第三个数去重
left++;//存入一组结果数值后,两个指针同时向中间收缩
right--;
}
}
}
return result;
}
};
哈希表版:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());//需要先对数组排序,从小到大排,以便去重
for (int i = 0; i < nums.size(); i++) {
//第一个元素数值就已经>0,证明不可能为结果要的三元组,可以直接返回了(因为nums已是从小到大排好序的)
if (nums[i] > 0) return result;
//可选项的前三个元素数值和已>0,后面的任何组合之和都不可能比该值小,所以也可以直接返回了
if (i + 2 < nums.size() && nums[i] + nums[i + 1] + nums[i + 2] > 0) break;
//必须是当前数与前一个数比较是否相等
//若是与当前数的后一个数比较的话会将当前数的组合也一起漏掉,不可以漏掉
if (i > 0 && nums[i] == nums[i - 1]) continue;//a去重
unordered_set<int> findC_set;
for (int j = i + 1; j < nums.size(); j++) {
//注意这里必须是检查3个数都一样的情况下才能去重,否则碰到[-2,0,1,1,2]用例时,会将结果(-2,1,1)也跳过了
if (j - 1 > i + 1 && nums[j] == nums[j - 1] && nums[j] == nums[j - 2]) continue;//b去重
int c = -nums[i] - nums[j];
if (findC_set.find(c) != findC_set.end()) {
result.push_back(vector<int>{nums[i], nums[j], c});//有合适的组合
findC_set.erase(c);//已使用的数值删除(由于是排列过的,找到一个c后,把set中对应的b删除),c去重
}
else {
findC_set.insert(nums[j]);
}
}
}
return result;
}
};
1.6 四数之和
给你一个由
n
个整数组成的数组nums
,和一个目标值target
。请你找出并返回满足下述全部条件且不重复的四元组[nums[a], nums[b], nums[c], nums[d]]
(若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a
、b
、c
和d
互不相同nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for (int k = 0; k < nums.size(); k++) {
//(剪枝处理)target>0的话,只要第一个数值是>target,则后面任何组合都一定只会更大,不符合要求,所以可直接退出(nums已是从小到大排好序的)
if (nums[k] > target && target >= 0) break;
if (k > 0 && nums[k] == nums[k - 1]) continue;//第一个元素去重
if (k - 3 < nums.size() && (long)nums[k] + nums[k - 1] + nums[k - 2] + nums[k - 3] > target&&target > 0) break;
for (int i = k + 1; i < nums.size(); i++) {
if (i - 1 > k && nums[i] == nums[i - 1]) continue;//第二个元素去重
if (nums[k] + nums[i] > target && target >= 0) break;//二级剪枝处理
if (nums[i] > target && target > 0) break;
int left = i + 1;
int right = nums.size() - 1;
while (left < right) {
//不加long的话会存在溢出
if ((long)nums[k] + nums[i] + nums[left] + nums[right] < target) left++;
else if ((long)nums[k] + nums[i] + nums[left] + nums[right] > target) right--;
else {
result.push_back(vector<int>{nums[k], nums[i], nums[left], nums[right]});
while (left < right && nums[left] == nums[left + 1]) left++;//第三个元素去重
while (left < right && nums[right] == nums[right - 1]) right--;//第四个元素去重
left++;//存入一组结果后两个指针同时向中间收缩
right--;
}
}
}
}
return result;
}
};
栈与队列
队列是先进先出,栈是先进后出。
栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。
所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)。
我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。
deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。
SGI STL中 队列底层实现缺省情况下一样使用deque实现的。
我们也可以指定vector为栈的底层实现,初始化语句如下:
std::stack<int, std::vector<int> > third; // 使用vector为底层容器的栈
队列同理:
队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构。
也可以指定list 为起底层实现,初始化queue的语句如下:
std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列
所以STL 队列也不被归类为容器,而被归类为container adapter( 容器适配器)。
游戏开发可能使用栈结构,编程语言的一些功能实现也会使用栈结构,实现函数递归调用就需要栈,但不是每种编程语言都支持递归,例如:
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
相信大家应该遇到过一种错误就是栈溢出,系统输出的异常是
Segmentation fault
(当然不是所有的Segmentation fault
都是栈溢出导致的) ,如果你使用了递归,就要想一想是不是无限递归了,那么系统调用栈就会溢出。
引入头文件:
#include <stack>
#include <queue>
1.1 用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(
push
、pop
、peek
、empty
):实现
MyQueue
类:
void push(int x)
将元素 x 推到队列的末尾int pop()
从队列的开头移除并返回元素int peek()
返回队列开头的元素boolean empty()
如果队列为空,返回true
;否则,返回false
说明:
- 你 只能 使用标准的栈操作 —— 也就是只有
push to top
,peek/pop from top
,size
, 和is empty
操作是合法的。- 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
class MyQueue {
public:
stack<int> stackIn;//输入栈
stack<int> stackOut;//输出栈
MyQueue() {
}
void push(int x) {
stackIn.push(x);
}
int pop() {
//只有当输出栈为空时,采从输入栈导入数据(导入输入栈的全部数据)
if (stackOut.empty()) {
while (!stackIn.empty()) {//从输入栈导入所有数据全部导入完
stackOut.push(stackIn.top());
stackIn.pop();
}
}
int result = stackOut.top();
stackOut.pop();
return result;
}
int peek() {
int result = pop();//直接调用已有的pop函数
stackOut.push(result);//因为pop里将元素弹出的,所以需要再重新添加
return result;
}
bool empty() {
//进栈和出栈都为空的话,说明队列为空了
return stackIn.empty() && stackOut.empty();
}
};
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue* obj = new MyQueue();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->peek();
* bool param_4 = obj->empty();
*/
1.2 用队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(
push
、top
、pop
和empty
)。实现
MyStack
类:
void push(int x)
将元素 x 压入栈顶。int pop()
移除并返回栈顶元素。int top()
返回栈顶元素。boolean empty()
如果栈是空的,返回true
;否则,返回false
。注意:
- 你只能使用队列的标准操作 —— 也就是
push to back
、peek/pop from front
、size
和is empty
这些操作。- 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
class MyStack {
public:
queue<int> que;
queue<int> queBackups;//辅助队列,仅用于备份
MyStack() {
}
void push(int x) {
que.push(x);
}
int pop() {
int size = que.size();
size--;//需保留最后一位元素
while (size--) {
queBackups.push(que.front());
que.pop();
}
int result = que.back();
que.pop();
que = queBackups;//把备份的内容给回que
while (!queBackups.empty()) {
queBackups.pop();//清空辅助队列
}
return result;
}
int top() {
//栈顶元素即为队列中最后入列的元素,即队列尾部元素
return que.back();
}
bool empty() {
return que.empty();
}
};
/**
* Your MyStack object will be instantiated and called as such:
* MyStack* obj = new MyStack();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->top();
* bool param_4 = obj->empty();
*/
只用一个队列版本:(一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时再去弹出元素就是栈的顺序了。)
class MyStack {
public:
queue<int> que;
MyStack() {
}
void push(int x) {
que.push(x);
}
int pop() {
int size = que.size();
size--;
//转(不完整的)一圈使原来的头部元素变成尾部元素
//即 使原来最后入列的元素成为 转完圈后的 最早入列的元素
while (size--) {
que.push(que.front());
que.pop();
}
//此时 所要的 原来位于栈顶的元素已转移到队列出口的位置
int result = que.front();
que.pop();//拿到值后记得将元素移除
return result;
}
int top() {
//栈顶元素即为队列中最后入列的元素,即队列尾部元素
return que.back();
}
bool empty() {
return que.empty();
}
};
/**
* Your MyStack object will be instantiated and called as such:
* MyStack* obj = new MyStack();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->top();
* bool param_4 = obj->empty();
*/
1.3 有效的括号
给定一个只包括
'('
,')'
,'{'
,'}'
,'['
,']'
的字符串s
,判断字符串是否有效。有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
class Solution {
public:
bool isValid(string s) {
if (s.size() % 2 != 0) return false;//字符串的长度为奇数,则一定会不匹配
stack<char> st;
for (char c : s) {
//switch (c) {
//case '{':st.push('}'); break; //记录左括号要找的另一半
//case '[':st.push(']'); break; //记录左括号要找的另一半
//case '(':st.push(')'); break; //记录左括号要找的另一半
//default://字符串中遍历到的字符为右括号中的某一个
// if (st.empty()) return false; //匹配过程中出现碰到字符串中为右括号但栈已经空了,代表右括号冗余,直接false
// if (c == st.top()) st.pop(); //有匹配的一对括号则对应消除
// else return false; //匹配过程中出现有右括号与左括号不对应,直接false
//}
if (c == '{') st.push('}'); //记录左括号要找的另一半
else if (c == '[') st.push(']'); //记录左括号要找的另一半
else if (c == '(') st.push(')'); //记录左括号要找的另一半
else {//字符串中遍历到的字符为右括号中的某一个
if (st.empty()) return false; //匹配过程中出现碰到字符串中为右括号但栈已经空了,代表右括号冗余,直接false
if (c == st.top()) st.pop(); //有匹配的一对括号则对应消除
else return false; //匹配过程中出现有右括号与左括号不对应,直接false
}
}
//遍历字符串后栈为空 ,则代表 匹配结束且左右括号已全部对应消除
//遍历字符串后栈不为空,则代表 有左括号冗余
return st.empty();
}
};
switch-case 版本:
class Solution {
public:
bool isValid(string s) {
if (s.size() % 2 != 0) return false;//字符串的长度为奇数,则一定会不匹配
stack<char> st;
for (char c : s) {
switch (c) {
case '{':st.push('}'); break; //记录左括号要找的另一半
case '[':st.push(']'); break; //记录左括号要找的另一半
case '(':st.push(')'); break; //记录左括号要找的另一半
default://字符串中遍历到的字符为右括号中的某一个
if (st.empty()) return false; //匹配过程中出现碰到字符串中为右括号但栈已经空了,代表右括号冗余,直接false
if (c == st.top()) st.pop(); //有匹配的一对括号则对应消除
else return false; //匹配过程中出现有右括号与左括号不对应,直接false
}
//if (c == '{') st.push('}'); //记录左括号要找的另一半
//else if (c == '[') st.push(']'); //记录左括号要找的另一半
//else if (c == '(') st.push(')'); //记录左括号要找的另一半
//else {//字符串中遍历到的字符为右括号中的某一个
// if (st.empty()) return false; //匹配过程中出现碰到字符串中为右括号但栈已经空了,代表右括号冗余,直接false
// if (c == st.top()) st.pop(); //有匹配的一对括号则对应消除
// else return false; //匹配过程中出现有右括号与左括号不对应,直接false
//}
}
//遍历字符串后栈为空 ,则代表 匹配结束且左右括号已全部对应消除
//遍历字符串后栈不为空,则代表 有左括号冗余
return st.empty();
}
};
1.4 删除字符串中的所有相邻重复项
给出由小写字母组成的字符串
S
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
class Solution {
public:
string removeDuplicates(string s) {
stack<char> st;
for (char c : s) {
if (st.empty()) st.push(c);//栈为空时直接压入
else if (st.top() == c) st.pop();//栈不为空且待压入值与栈顶值相同,直接弹出
else st.push(c);//栈不为空且待压入值与栈顶值不重复,可直接压入
}
string result;//存放栈中值
while (!st.empty()) {
result.push_back(st.top());
st.pop();//存顺便将栈中值删除
}
//栈中数据拿出来后顺序是相反的,需反转恢复原有顺序
reverse(result.begin(), result.end());
return result;
}
};
1.5 逆波兰表达式求值
给你一个字符串数组
tokens
,表示一个根据 逆波兰表示法 表示的算术表达式。请你计算该表达式。返回一个表示表达式值的整数。
注意:
- 有效的算符为
'+'
、'-'
、'*'
和'/'
。- 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
- 两个整数之间的除法总是 向零截断 。
- 表达式中不含除零运算。
- 输入是一个根据逆波兰表示法表示的算术表达式。
- 答案及所有中间计算结果可以用 32 位 整数表示。
思路:
若当前字符为变量或者为数字,则压栈;
若是运算符,则将栈顶两个元素弹出作相应运算,结果再入栈;
最后当表达式扫描完后,栈里的就是结果。
题外话:
其实逆波兰表达式相当于是二叉树中的后序遍历。 也就是说,可以把运算符作为中间节点,按照后序遍历的规则画出一个二叉树。
中缀表达式,如 4 + 13 / 5 ,是习惯看到的表达式。
后缀表达式,如 ["4", "13", "5", "/", "+"],也就是逆波兰表达式,对计算机来说是更友好。
因为计算机可以利用栈来顺序处理,不需要考虑优先级了。也不用回退了。
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> result;
for (string s : tokens) {
if (s == "+" || s == "-" || s == "*" || s == "/") {
int num1 = result.top();
result.pop();
int num2 = result.top();
result.pop();
//注意运算时不要把数字1和数字2弄反了
//正常是前数-后数,但栈是先进后出,因为前数后数顺序就反了
//出栈后需要:后出的数-先出的数
//num2后出,因此:num2-num1 。
switch (s[0]) {
case '+':result.push(num2 + num1); break;
case '-':result.push(num2 - num1); break;
case '*':result.push(num2 * num1); break;
case '/':result.push(num2 / num1); break;
}
}
else result.push(stoi(s));
}
return result.top();
}
};