链表
1.移除链表元素(203)
题目
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1
输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7
输出:[]
提示:
- 列表中的节点数目在范围
[0, 104]
内 1 <= Node.val <= 50
0 <= val <= 50
思路及题解
解法一 原链表删除元素
- 删除某个节点,只需要上一个节点的 next 指针指向要删除的节点的下一个节点即可
- 当下一个节点为空时,遍历结束,停止删除
- 当头节点需要删除时,直接把 head 向前移动即可
- 头节点为空时,直接返回
- 可以优化的地方
- pre指针可以不用,用 cur 、 cur -> next 、cur -> next -> next 表示即可
- 头节点为空和不为空时操作方法不统一,需要用虚拟头节点
/**
* 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 (head != NULL && head->val == val) {
ListNode* p =head;
head = head->next;
// free(p); 注意c++用 delete 释放内存空间,c 才用 free
delete p;
}
//头节点为空时
if (head == NULL)
return head;
ListNode* pre = head;
ListNode* cur = head->next;
while (cur != NULL) {
if (cur->val == val) {
pre->next = cur->next;
ListNode* p = cur;
cur = pre->next;
// free(p);
delete p;
}
else{
pre = pre->next;
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; // 记得开辟内存空间
dummyHead->next = head;
ListNode* cur = dummyHead;
while (cur->next != NULL) {
if (cur->next->val == val) {
ListNode* p = cur->next;
cur->next = cur->next->next;
delete p;
} else { // 如果下一个节点还是 val 就一直删除,否则才向前
cur = cur->next;
}
}
// 恢复头节点
head = dummyHead->next;
delete dummyHead;
return head;
}
};
2.设计链表(707)
题目
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:val
和 next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。
如果是双向链表,则还需要属性 prev
以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
实现 MyLinkedList
类:
MyLinkedList()
初始化MyLinkedList
对象。int get(int index)
获取链表中下标为index
的节点的值。如果下标无效,则返回-1
。void addAtHead(int val)
将一个值为val
的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。void addAtTail(int val)
将一个值为val
的节点追加到链表中作为链表的最后一个元素。void addAtIndex(int index, int val)
将一个值为val
的节点插入到链表中下标为index
的节点之前。如果index
等于链表的长度,那么该节点会被追加到链表的末尾。如果index
比长度更大,该节点将 不会插入 到链表中。void deleteAtIndex(int index)
如果下标有效,则删除链表中下标为index
的节点。
示例:
输入
["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"]
[[], [1], [3], [1, 2], [1], [1], [1]]
输出
[null, null, null, null, 2, null, 3]
解释
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addAtHead(1);
myLinkedList.addAtTail(3);
myLinkedList.addAtIndex(1, 2); // 链表变为 1->2->3
myLinkedList.get(1); // 返回 2
myLinkedList.deleteAtIndex(1); // 现在,链表变为 1->3
myLinkedList.get(1); // 返回 3
提示:
0 <= index, val <= 1000
- 请不要使用内置的 LinkedList 库。
- 调用
get
、addAtHead
、addAtTail
、addAtIndex
和deleteAtIndex
的次数不超过2000
。
思路及题解
解法一 单项链表
- 使用虚拟头节点,增加和删除元素变得更容易
- 下面的代码中,for循环可以用while循环优化,代码更简洁
- 注意C++内定义结构体的方式、在类内要创建成员变量、MyLinkedList实际上是构造器
class MyLinkedList {
public:
// 定义链表节点结构体
struct LinkedNode {
int val;
LinkedNode* next;
//可以写LinkedNode(int val),代码更简洁
//下面的代码是在写这个结构体之前写的,所以新节点还要指向NULL是多余写法
LinkedNode() : val(0), next(nullptr) {}
};
int size;
LinkedNode* head;
MyLinkedList() {
head = new LinkedNode();
size = 0;
}
int get(int index) {
if (index >= size || index < 0)
return -1;
LinkedNode* cur = head->next;
for (int i = 0; i < index; i++) {
cur = cur->next;
}
// while 的写法:
/* while(index--){ // 如果--index 就会陷入死循环
cur = cur->next;}
*/
return cur->val;
}
void addAtHead(int val) {
LinkedNode* Node = new LinkedNode();
Node->val = val;
Node->next = head->next;
head->next = Node;
size++;
}
void addAtTail(int val) {
LinkedNode* Node = new LinkedNode();
Node->val = val;
Node->next = NULL;
LinkedNode* cur = head;
for (int i = 0; i < size; i++)
cur = cur->next;
// while 的写法
/* while(cur->next != nullptr){
cur = cur->next;
}
*/
cur->next = Node;
size++;
}
void addAtIndex(int index, int val) {
if (index > size)
return;
LinkedNode* cur = head;
LinkedNode* Node = new LinkedNode();
Node->val = val;
for (int i = 0; i < index && i < size; i++)
cur = cur->next;
/* while(index--) {
cur = cur->next;
}
*/
Node->next = cur->next;
cur->next = Node;
size++;
}
void deleteAtIndex(int index) {
if (index >= size || index < 0) //注意这里等于 size 也不行
return;
LinkedNode* cur = head;
/*while (index--) {
cur = cur->next;
}*/
for(int i=0;i<index;i++)
cur = cur->next;
LinkedNode* p = cur->next;
cur->next = p->next;
delete p;
//注意释放内存后还要将其变为空指针,否则p为野指针,若一不小心使用后果不堪设想
p = NULL;
size--;
}
};
/**
* 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);
*/
3.反转链表(207)
题目
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2]
输出:[2,1]
示例 3:
输入:head = []
输出:[]
提示:
- 链表中节点的数目范围是
[0, 5000]
-5000 <= Node.val <= 5000
**进阶:**链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?
思路及题解
解法一 迭代法(双指针)
- 原地操作,改变next的指向,不浪费空间
- 需要创建临时指针 p 指向 cur 的下一个节点,否则 cur 无法向前转移
/**
* 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) {
//if(head == NULL) return head;
ListNode* pre = NULL;
ListNode* cur = head;
ListNode* p = NULL;
while(cur != NULL){
p = cur -> next;
cur->next = pre;
pre = cur;
cur = p;
}
return pre;
}
};
解法二 递归法
- 待完善
解法三 使用栈
- 待完善
4.两两交换链表中的节点(24)
题目
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
示例 2:
输入:head = []
输出:[]
示例 3:
输入:head = [1]
输出:[1]
提示:
- 链表中节点的数目在范围
[0, 100]
内 0 <= Node.val <= 100
思路及题解
解法一 模拟(迭代)
- 这道题正常迭代即可
- 关键是画图,明白修改 next 指针的过程和操作的先后顺序,否则容易乱
- 考虑好各种边界条件
- 注意清理内存
- 可以使用虚拟头节点,这样操作更加方便
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
//使用了两个指针 pre 和 cur
ListNode* pre = head;
if(!pre) return head;
ListNode* cur = head->next;
if(!cur) return head;
//切换头指针,实际上是指向原来第二个元素
head = cur;
while(true){
ListNode* p = new ListNode(0,pre);
pre->next = cur -> next;
cur->next = p->next;
pre = pre->next;
//在这里要判断一次退出条件
if(pre == nullptr || pre->next == nullptr) break;
cur->next->next = pre->next;
cur = pre->next;
//每次遍历要释放内存
delete p;
p = nullptr;
}
return head;
}
};
以下是优化过的版本:
//这个版本使用了虚拟头节点,并且切换next的操作更加简洁
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
dummyHead->next = head; // 将虚拟头结点指向head,这样方便后面做删除操作
ListNode* cur = dummyHead;
while(cur->next != nullptr && cur->next->next != nullptr) {
ListNode* tmp = cur->next; // 记录临时节点
ListNode* tmp1 = cur->next->next->next; // 记录临时节点
cur->next = cur->next->next; // 步骤一
cur->next->next = tmp; // 步骤二
cur->next->next->next = tmp1; // 步骤三
cur = cur->next->next; // cur移动两位,准备下一轮交换
}
ListNode* result = dummyHead->next;
delete dummyHead;
return result;
}
};
解法二 递归
- 待完善
5.删除链表的倒数第 N 个节点(19)
题目
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1
输出:[]
示例 3:
输入:head = [1,2], n = 1
输出:[1]
提示:
- 链表中结点的数目为
sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz
**进阶:**你能尝试使用一趟扫描实现吗?
思路及题解
解法一 双指针
- 双指针法的典型应用,右指针(快指针)先向右移动 n 位,左指针(慢指针)再和其同时移动,直到右指针指向末尾,删除左指针指向元素即可
- 注意使用虚拟头节点就可以统一逻辑!一定要习惯使用虚拟头节点!!!
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* left = head;
ListNode* right = head;
//移动右指针
for (int i = 0; i < n; i++)
right = right->next;
//没有使用虚拟头节点,需要处理特殊情况
if (right == nullptr)
return head->next;
while (right->next) {
left = left->next;
right = right->next;
}
//释放内存
ListNode* temp = left->next;
left->next = temp->next;
delete temp;
temp = nullptr;
return head;
}
};
用虚拟头节点的优化版:
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* slow = dummyHead;
ListNode* fast = dummyHead;
//学会这种遍历方式!!用上面解放的for有点蠢!
while(n-- && fast != NULL) {
fast = fast->next;
}
fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点
while (fast != NULL) {
fast = fast->next;
slow = slow->next;
}
slow->next = slow->next->next;
// ListNode *tmp = slow->next; C++释放内存的逻辑
// slow->next = tmp->next;
// delete tmp;
return dummyHead->next;
}
};
6.链表相交(160)
题目
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null
。
图示两个链表在节点 c1
开始相交**:**
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例 2:
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
提示:
listA
中节点数目为m
listB
中节点数目为n
0 <= m, n <= 3 * 104
1 <= Node.val <= 105
0 <= skipA <= m
0 <= skipB <= n
- 如果
listA
和listB
没有交点,intersectVal
为0
- 如果
listA
和listB
有交点,intersectVal == listA[skipA + 1] == listB[skipB + 1]
**进阶:**你能否设计一个时间复杂度 O(n)
、仅用 O(1)
内存的解决方案?
思路及题解
解法一 双指针
-
创建指针curA遍历A链表,curB遍历B链表,当 curA == curB 时,指针指向的节点即为相交节点(因为按next指针继续往前走,后面的指针都相同)
-
我们可以先将两个链表遍历一遍,得到两个链表的长度,再计算出长度差值,让较短的链表与较长的链表对齐,保证尾部对齐(因为如果相交,他们必然是同一个尾部)。然后两个指针同时向前,返回指向相同节点的指针。如果没有这个指针,返回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 countA = 0, countB = 0; while (curA) { curA = curA->next; countA++; } while (curB) { curB = curB->next; countB++; } curA = headA; curB = headB; int sub; if (countA >= countB) { sub = countA - countB; while (sub--) { curA = curA->next; } } else { sub = countB - countA; while (sub--) { curB = curB->next; } } while (curA && curB) { if (curA == curB) return curA; curA = curA->next; curB = curB->next; } return NULL; } };
解法二 哈希表
- 待完善
解法三 另一种双指针
- 待完善
7.环形链表(142)
题目
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。
提示:
- 链表中节点的数目范围在范围
[0, 104]
内 -105 <= Node.val <= 105
pos
的值为-1
或者链表中的一个有效索引
**进阶:**你是否可以使用 O(1)
空间解决此题?
思路及题解
解法一 双指针,环形链表典型解法
解决这道题,实际上要解决的是两个问题
- 链表是否是环形链表(是否有环)
- 这个环形链表的入口在哪里
先解决第一个问题:判断一个链表是否是环形链表
如图所示,只需设置一个快指针和一个慢指针。假设快指针每次走两步,慢指针每次走一步,如果有环,那么快慢指针一定会在环里相遇。几个问题如下:
- 如果没有环,那么快指针会直接指向空
- 快指针可以在环内绕好几圈,而慢指针还没有进环
- 慢指针一旦进环,快指针一定在慢指针绕完一圈之前追上它。因为快指针速度是慢指针两倍,快指针绕完一圈,慢指针只绕了半圈
- 快指针不会越过慢指针。因为等价于每次移动,快指针向慢指针靠近了一个节点的距离。若快指针每次走三步,慢指针每次走两步,每次快指针向慢指针靠近两个节点的距离,那么快指针就可能越过慢指针
- 当快指针指向的节点等于慢指针指向的节点时,证明链表有环
第二个问题:找到环形链表的入口
作以下数学推导:
假设头节点离环形入口节点的距离为 x ,fast指针与slow指针相遇节点离环形入口节点为 y,剩下的具体为 z ,如图所示
由快指针的速度是慢指针的两倍得时间相同,慢指针走过的距离的两倍等于快指针走过的距离
又因为快指针可能已经在环里转了很多圈,假设转了 n 圈
得式子:2(x + y) = x + n(y + z)
化简得:x = n(y + z) - 2y(要计算出 x 的长度,得到入口位置)
由于“-2y”在图中意义不明,我们将这个式子再次化简,将括号拆开,得: x = (n-1)(y+z) + z
注意 n 大于等于 1,因为快指针至少走一圈才能遇见慢指针
当 n=1 时,有 x=z,即如果在相遇节点定义一个指针 index1,在头节点定义一个指针 index2,两个指针同时前进,最后会在环形入口节点处相遇
当 n > 1时,等价于多走了 n -1 个 y+z 的距离,也就是多转了 n -1 圈,最后两个指针还是会在环形入口节点处相遇
故当两个指针指向同一个节点时,我们认为这个节点是环形入口节点
/**
* 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;
//判断是否是环形链表,不是返回NULL
while (fast && fast->next) {
fast = fast->next->next;
slow = slow->next;
if (fast == slow) {
//快慢指针相遇,是环形链表,设两个指针,两指针向前,指向同一节点时停止
ListNode* p = fast;
ListNode* q = head;
while (p != q) {
p = p->next;
q = q->next;
}
//p指向同一节点,返回该节点
return p;
}
}
return NULL;
}
};
- 时间复杂度:O(n),快慢指针相遇和两个指针p、q相遇时走的长度均小于链表长度,加起来长度小于2n
- 空间复杂度:O(1),在链表上原地操作
解法二 哈希表
遍历链表,用哈希表存储每次遇到的 ListNode 的指针(key),value 值为出现的次数,当出现次数大于1时,该节点为环形链表入口。若没有这种节点,返回NULL
/**
* 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) {
map<ListNode*,int> map;
while(head!=NULL){
map[head]++;
if(map[head] > 1) return head;
head = head->next;
}
return NULL;
}
};
- 时间复杂度:O(n),遍历了一次链表
- 空间复杂度:O(n),使用了哈希表,将链表中所有节点都保存进去
解法三 两层遍历(暴力解法)
设置两个指针,一个向前遍历,一个留在原地判断。当两个指针重合,说明这个节点是环形链表入口。当遍历的指针遍历超过10000次节点时(节点的最大数目),停止遍历这个节点,换成下一个节点。
该方法较为耗时
/**
* 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* p =head;
ListNode* q =head;
int count=0;
while(p!=NULL){
q = p;
count = 0;
while(q!=NULL){
q = q->next;
if(q == p) return p;
if(count++ > 10000) break;
}
if(q == NULL) return NULL;
p = p->next;
}
return NULL;
}
};
- 时间复杂度:O(n2),两层遍历
- 空间复杂度:O(1)
总结
哈希表
-
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法
-
在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
集合 底层实现 是否有序 数值是否可以重复 能否更改数值 查询效率 增删效率 std::set 红黑树 有序 否 否 O(log n) O(log n) std::multiset 红黑树 有序 是 否 O(logn) O(logn) std::unordered_set 哈希表 无序 否 否 O(1) O(1) -
map:
映射 底层实现 是否有序 数值是否可以重复 能否更改数值 查询效率 增删效率 std::map 红黑树 key有序 key不可重复 key不可修改 O(logn) O(logn) std::multimap 红黑树 key有序 key可重复 key不可修改 O(log n) O(log n) std::unordered_map 哈希表 key无序 key不可重复 key不可修改 O(1) O(1) -
选取原则(优先级从上到下):
- 数据范围较小的集合——数组
- 数据范围较大较零散集合,无序:unordered_set
- 集合有序:set,元素有重复:multiset
- 键值对无序:unordered_map
- 键值对有序:map,元素有重复:multimap
1.有效的字母异位词(242)
题目
给定两个字符串 *s*
和 *t*
,编写一个函数来判断 *t*
是否是 *s*
的字母异位词。
**注意:**若 *s*
和 *t*
中每个字符出现的次数都相同,则称 *s*
和 *t*
互为字母异位词。
示例 1:
输入: s = "anagram", t = "nagaram"
输出: true
示例 2:
输入: s = "rat", t = "car"
输出: false
提示:
1 <= s.length, t.length <= 5 * 104
s
和t
仅包含小写字母
进阶: 如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?
思路及题解
解法一 排序
将两个字符串排序,比较两个字符串是否相等,相等即为字母异位词,反之不是。注意先判断字符串长度是否相等,不相等则一定不是字母异位词
class Solution {
public:
bool isAnagram(string s, string t) {
if (s.size() != t.size())
return false;
sort(s.begin(), s.end());
sort(t.begin(), t.end());
return s == t;
}
};
时间复杂度:比较字符串是否相等O(n),sort内部用快速排序O(n log n),整体复杂度O(n log n)
空间复杂度:快速排序本质是二叉树,需要O(log n)大小空间
解法二 哈希表——map
用两个map,key为字符串的每个字母,value为字母出现的次数,比较每个字母出现的次数是否相等,占用空间较大,不是哈希表法的最佳方案
class Solution {
public:
bool isAnagram(string s, string t) {
if (s.size() != t.size())
return false;
map<char, int> m1;
map<char, int> m2;
for (int i = 0; i < s.size(); i++) {
m1[s[i]]++;
}
for (int i = 0; i < t.size(); i++) {
m2[t[i]]++;
}
for (int i = 0; i < s.size(); i++) {
if (m1[s[i]] != m2[s[i]])
return false;
}
return true;
}
};
时间复杂度:O(n),遍历两个字符串和比较字母出现次数都是这个复杂度
空间复杂度:O(n),即两个map的容量O(2n),最坏情况下需要存储两个字符串的所有字母
解法三 哈希表——数组
- 这里数据的范围较小(只有小写字母,26个),故考虑用数组实现哈希表
- 用 “ - ‘a’ ” 构建各个字母对数组下标的唯一映射,遍历两个字符串(事先判断长度是否相等,则可将两次遍历合并)。当字符串s中出现一次某字母,个数加一,当t中出现该字母,个数减一,最后再遍历一次数组,若数组中元素全为0,则说明两字符串是字母异位词
class Solution {
public:
bool isAnagram(string s, string t) {
//注意初始化是0而不是‘0’,否则最后遍历出来的值是0的ASCII码
int hash[26] = {0};
if (s.size() != t.size())
return false;
for (int i = 0; i < s.size(); i++) {
hash[s[i] - 'a']++;
hash[t[i] - 'a']--;
}
for (int i = 0; i < 26; i++) {
if (hash[i] != 0)
return false;
}
return true;
}
};
时间复杂度:O(n),遍历字符串
空间复杂度:O(1),数组大小空间
2.两个数组的交集(349)
题目
给定两个数组 nums1
和 nums2
,返回 它们的 交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的
提示:
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 1000
思路及题解
解法一 哈希表——unordered_set
-
注意题目特意说明:输出结果中的每个元素一定是唯一的,也就是说输出的结果的去重的, 同时可以不考虑输出结果的顺序
-
考虑用数组,如果没有限制数据的大小,则不能用数组,因为**如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。**但此题限定了数据的大小在1000以内,故 可以用数组
-
若不能用数组,则考虑set。这里不用考虑输出的顺序,因此可以用unordered_set,降低查询和增删的时间复杂度
-
用set而不用multiset:给结果去重
-
为什么遇到哈希问题不能直接都用set:
直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。
不要小瞧 这个耗时,在数据量大的情况,差距是很明显的。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> set;
vector<int> output;
//两次遍历,如果两个字符串对应的字母相同,则加入set中
for (int i = 0; i < nums1.size(); i++) {
for (int j = 0; j < nums2.size(); j++) {
if (nums1[i] == nums2[j])
set.insert(nums1[i]);
}
}
//set中的元素转移到vector中
for (unordered_set<int>::iterator it = set.begin(); it != set.end();
it++) {
output.push_back(*it);
}
return output;
}
};
- 时间复杂度:O(n2),遍历了两次字符串
- 空间复杂度:O(n)
- 可以优化的地方:
- 两次遍历,时间复杂度太大,可以将nums1的元素先存入一个set中,再用一次循环将nums2的元素与set进行对比
- set中的元素转移到vector中,不需要用遍历的方式,直接用迭代器拷贝即可
- 优化过的代码如下:
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set; // 存放结果
unordered_set<int> nums_set(nums1.begin(), nums1.end());
for (int num : nums2) {
// 用find函数找相同元素,注意没有找到的话find返回的是end迭代器
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
// 转换为vector返回
return vector<int>(result_set.begin(), result_set.end());
}
};
- 时间复杂度:O(n+m),多了vector的复杂度
- 空间复杂度:O(n)
解法二 哈希表——数组
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> res;
int hash[1000] = {0};
for (int num : nums1) {
hash[num]++;
}
for (int num : nums2) {
if (hash[num] >= 1) {
res.insert(num);
}
}
return vector<int>(res.begin(), res.end());
}
};
- 时间复杂度:O(n+m)
- 空间复杂度:O(n)
3.快乐数(202)
题目
编写一个算法来判断一个数 n
是不是快乐数。
「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n
是 快乐数 就返回 true
;不是,则返回 false
。
示例 1:
输入:n = 19
输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
示例 2:
输入:n = 2
输出:false
提示:
1 <= n <= 231 - 1
思路及题解
解法一 哈希表——set
- 注意到题目告诉我们 sum 会无限循环,由此联想到判断一个数是否出现过——用哈希表
- 将每次求和的数值进行判断,如果sum是1,返回true。否则判断其有没有在set出现过**(常用find函数)**,如果出现过,返回false,否则将其插入到set中
- 注意求一个数字各位和(平方和)的方法
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> set;
while (true) {
int sum = getSum(n);
if (sum == 1)
return true;
else {
if (set.find(sum) != set.end())
return false;
else {
set.insert(sum);
n = sum;
}
}
}
}
};
- 时间复杂度:O(log n)
- 空间复杂度:O(log n)
4.两数之和(1)
题目
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
提示:
2 <= nums.length <= 104
-109 <= nums[i] <= 109
-109 <= target <= 109
- 只会存在一个有效答案
**进阶:**你可以想出一个时间复杂度小于 O(n2)
的算法吗?
思路及题解
解法一 暴力解法
两个for循环即可,不再赘述
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int size = nums.size();
for (int i = 0; i < size - 1; i++) {
for (int j = i + 1; j < size; j++) {
if (nums[i] + nums[j] == target)
return { i, j };
}
}
return {};
}
};
- 时间复杂度:O(n2)
- 空间复杂度:O(1)
解法二 哈希表——map
本题有四个重点:
- 为什么会想到用哈希表:当要知道遍历到的元素是否之前出现过,考虑用哈希表。本题求两数的和是否为 target ,可以转化为 target 减去 遍历到的数 是否在之前出现过,故想到用哈希表
- 哈希表为什么用map:为了方便找到之前出现过的数的下标,便于输出,所以用map来存放
- 本题map是用来存什么的:存放之前遍历过的数
- map中的key和value用来存什么的:key存放数的数值,value存放数的下标。因为用find函数查找时查找的是数的数值,故数值要放到key中
由于本题对顺序没有要求,故用 unordered_map 即可
遍历各个元素,如果差值在之前出现过,返回。否则存放到 map 中
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> map;
for (int i = 0; i < nums.size(); i++) {
unordered_map<int, int>::iterator it = map.find(target - nums[i]);
if (it == map.end()) {
map.insert(pair<int, int>(nums[i], i));
} else {
return {it->second, i};
}
}
return {};
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(n)
5.四数相加 II(454)
题目
给你四个整数数组 nums1
、nums2
、nums3
和 nums4
,数组长度都是 n
,请你计算有多少个元组 (i, j, k, l)
能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
示例 1:
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出:2
解释:
两个元组如下:
1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0
示例 2:
输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1
提示:
n == nums1.length
n == nums2.length
n == nums3.length
n == nums4.length
1 <= n <= 200
-228 <= nums1[i], nums2[i], nums3[i], nums4[i] <= 228
思路及题解
解法一 哈希表——map
- 暴力解法,4个for循环,时间复杂度为O(n4),时间复杂度极差
- 考虑将四个数相加拆分成两部分,一次是 a+b 的遍历,一次是 c+d 的遍历,这样时间复杂度就降低到了O(n2)
- 为什么不是拆分成 a 和 b+c+d 呢?因为遍历 b+c+d 的时间复杂度为O(n3),时间复杂度比两个O(n2)要高
- 要使 a+b+c+d 的值等于0,则要找到 0-(c+d) 的值是否在 a+b 的遍历中出现过——哈希表
- 有很多种a、b的组合和是相同的,都可以算作元组中的一部分,因此要统计某个 a+b 的值出现过的次数——用map
- 本题不涉及顺序,故使用unordered_map
- count为元组的个数,注意找到符合条件的 a+b 的值是count不是加一,而是加上value即该 a+b 的值出现过的次数,因为每个次数代表一种a和b的组合,都可以组成一个元组
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3,
vector<int>& nums4) {
unordered_map<int, int> map;
int count = 0;
//循环可以使用增强for循环来优化
for (int i = 0; i < nums1.size(); i++) {
for (int j = 0; j < nums1.size(); j++) {
map[nums1[i] + nums2[j]]++;
}
}
for (int i = 0; i < nums1.size(); i++) {
for (int j = 0; j < nums1.size(); j++) {
unordered_map<int, int>::iterator it =
map.find(0 - (nums3[i] + nums4[j]));
if (it != map.end()) {
count += it->second;
}
}
}
return count;
}
};
- 时间复杂度:O(n2)
- 空间复杂度:O(n2),最坏情况下a和b元素均不同,a+b有n2个值
6.赎金信(383)
题目
给你两个字符串:ransomNote
和 magazine
,判断 ransomNote
能不能由 magazine
里面的字符构成。
如果可以,返回 true
;否则返回 false
。
magazine
中的每个字符只能在 ransomNote
中使用一次。
示例 1:
输入:ransomNote = "a", magazine = "b"
输出:false
示例 2:
输入:ransomNote = "aa", magazine = "ab"
输出:false
示例 3:
输入:ransomNote = "aa", magazine = "aab"
输出:true
提示:
1 <= ransomNote.length, magazine.length <= 105
ransomNote
和magazine
由小写英文字母组成
思路及题解
解法一 哈希表——数组
- 找magazine里出现的字母是否在ransomNote中出现过——哈希表
- 数值的范围不大,限定在小写字母范围内——数组
- 创建一个数组,先遍历ransomNote,通过减去‘a’的操作将字母与数组元素一一映射,出现的字母对应的位置值加一
- 再遍历magazine,出现的字母对应数值减1,最后再遍历hash数组,若出现大于0的值,说明magazine里缺少ransomNote中的某个字母,返回false
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int hash[26] = {0};
//扣细节,可以先比较ransomNote和magazine的长度,若后者小于前者,可以直接返回false
for (int i = 0; i < ransomNote.size(); i++) {
hash[ransomNote[i] - 'a']++;
}
for (int i = 0; i < magazine.size(); i++) {
hash[magazine[i] - 'a']--;
}
for (int i = 0; i < 26; i++) {
if (hash[i] > 0)
return false;
}
return true;
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(1),数组的大小
7.三数之和(15)
题目
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请
你返回所有和为 0
且不重复的三元组。
**注意:**答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。
提示:
3 <= nums.length <= 3000
-105 <= nums[i] <= 105
思路及题解
解法一 哈希表——set
- 类似之前的题目四数相加II,可以两次遍历a和b的值,再将0-(a+b)的值与哈希表中的值(事先存入c)做对比,看是否出现过
- 这样的思路本来没有问题,但复杂在去重的操作极其麻烦,需要考虑的条件非常多,而且去重的过程消耗的时间非常长,这里只把代码随想录里的代码附上
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[j], c = -(a + b)
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么不可能凑成三元组
if (nums[i] > 0) {
break;
}
if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重
continue;
}
unordered_set<int> set;
for (int j = i + 1; j < nums.size(); j++) {
if (j > i + 2
&& nums[j] == nums[j-1]
&& nums[j-1] == nums[j-2]) { // 三元组元素b去重
continue;
}
int c = 0 - (nums[i] + nums[j]);
if (set.find(c) != set.end()) {
result.push_back({nums[i], nums[j], c});
set.erase(c);// 三元组元素c去重
} else {
set.insert(nums[j]);
}
}
}
return result;
}
};
- 时间复杂度:O(n2)
- 空间复杂度:O(n)
解法二 双指针法
- 实现思路:首先将数组排序,方便进行后续操作
- 循环遍历 i ,i 即为a,每次遍历再设置两个指针left和right,分别作为b和c,left初始值为 i + 1,right 初始值为 nums.size() -1.
- 注意如果 nums[ i ] 大于0,则立即结束遍历,因为数组已经排好序,三数之和必定大于0
- a的去重操作:因为每次遍历时,a为这个值的所有可能三元组均已被加入数组中,所以如果下个a(即下个i)指向的数和上一个数相同,则这次遍历不进行,直接 continue。因为数组已经排好序,所以后面能执行遍历的 i 必定没有执行过
- 注意a去重时不能将这次要遍历的数和下次要便利的数进行比较,应该是这次要遍历的数与上次遍历过的数进行比较,否则会漏掉这次的遍历
- 循环条件为 right < left,因为两个指针指向同一个数时,不可能得到符合条件的三元组(三个数互相不是同一个数,即下标不同)
- 计算 nums[ i ] + nums[ left ] + nums[ right ]的值,如果这个值大于0,说明三数相加的值太大,right需要向左移动一位(设定每次遍历 i 不动,left只能向右移动,right 只能向左移动)。如果小于0,left向右移动一位,直到值为0,将这个三元组加入数组中
- 加入数组后进行b和c的去重操作,如果 left 的后一位(或right的前一位)数值和本次遍历数值相同,同a去重的道理,所有b(c)为这个值的所有可能三元组均已被加入数组中,故继续向右(向左)移动
- 注意b和c的去重要在加入过一次三元组后进行,否则会漏掉
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); i++) {
if (nums[i] > 0)
return res;
if (i > 0 && nums[i] == nums[i - 1])
continue;
// 错误去重a方法,将会漏掉-1,-1,2 这种情况
/*
if (nums[i] == nums[i + 1]) {
continue;
}
*/
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
// 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组
/*
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
*/
if (nums[i] + nums[left] + nums[right] > 0) {
right--;
} else if (nums[i] + nums[left] + nums[right] < 0) {
left++;
} else {
res.push_back({nums[i], nums[left], nums[right]});
while (right > left && nums[right] == nums[right - 1])
right--;
while (right > left && nums[left] == nums[left + 1])
left++;
// 找到答案时,双指针同时收缩
right--;
left++;
}
}
}
return res;
}
};
- 时间复杂度: O(n^2)
- 空间复杂度: O(1)
思考
两数之和 就不能使用双指针法,因为两数之和要求返回的是索引下标, 而双指针法一定要排序,一旦排序之后原数组的索引就被改变了。
如果两数之和要求返回的是数值的话,就可以使用双指针法了。
8.四数之和(18)
题目
给你一个由 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
你可以按 任意顺序 返回答案 。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]
提示:
1 <= nums.length <= 200
-109 <= nums[i] <= 109
-109 <= target <= 109
思路及题解
解法一 双指针法
-
延续三数之和的思路即可
-
四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n2),四数之和的时间复杂度是O(n3) 。
那么一样的道理,五数之和、六数之和等等都采用这种解法。(即双指针只能处理两层for循环,剩下的还是要依靠for循环来遍历)
对于三数之和双指针法就是将原本暴力O(n3)的解法,降为O(n2)的解法,四数之和的双指针解法就是将原本暴力O(n4)的解法,降为O(n3)的解法。
-
这里特别要注意的是剪枝的操作,不能用像三数之和的 nums[ i ] > 0 的写法,因为这里改成了 target ,但也不能写 nums[i] > target ,因为有可能 target 是个负数,nums[ i ]若是比他大的负数,n后面的数是正数,也可能得到 target 值,故剪枝时需要加上条件:nums[ i ] 大于 0,这样nums [ i ] 后面的数也都是正数(已经排好序),相加必然也超过 target 值
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size(); i++) {
// if(nums[i] > target) return res; 不能有这个条件
//正确剪枝操作
if (nums[i] > target && nums[i] >= 0)
//这里 break 掉,统一用最后的 return 返回
break;
//去重操作
if (i > 0 && nums[i] == nums[i - 1])
continue;
for (int j = i + 1; j < nums.size(); j++) {
//也是同样的剪枝操作
if (nums[i] + nums[j] > target && nums[i] + nums[j] >= 0)
break;
if (j > i + 1 && nums[j] == nums[j - 1])
continue;
int left = j + 1;
int right = nums.size() - 1;
while (right > left) {
// 注意加long,因为相加的数可能超过 int 的范围!!!!!!
if ((long)nums[i] + nums[j] + nums[left] + nums[right] >
target)
right--;
else if ((long)nums[i] + nums[j] + nums[left] +
nums[right] <
target)
left++;
else {
res.push_back(
{nums[i], nums[j], nums[left], nums[right]});
while (right > left && nums[right - 1] == nums[right])
right--;
while (right > left && nums[left + 1] == nums[left])
left++;
left++;
right--;
}
}
}
}
return res;
}
};
- 时间复杂度:O(n3)
- 空间复杂度:O(1)