这一节,并不要求掌握内部结构,只需要在刷题需要时会使用即可。
C++中,哈希表是通过STL的std::unordered_map
来表示的。它是一个关联容器,存储了键-值对,并通过哈希函数对键进行散列来实现快速的增删改查。由于哈希函数的使用,这种数据结构在平均情况下具有常数时间复杂度 O ( 1 ) O(1) O(1),但还是复杂度比数组随机访问要高得多。
使用哈希表,可简单的理解为它是一种无序的集合结构,key值不能重复
如果只有键,没有值,用std::unordered_set
[ ], insert(make_pair())
删:erase(),clear()
改:[ ]
查:find(), count()
#include
#include
int main(){
std::unordered_set<string> hashSet;
hashSet.insert("apple");
hashSet.insert("banana");
hashSet.erease("banana");
if (hashSet.find("apple") != hasSet.end())
std::cout << "有apple" << std::endl;
if (hashSet.count("apple") > 0)
std::cout << "有apple" << std::endl;
return 0;
}
键-值对,用std::unordered_map
insert()
删:erase(),clear()
改:没有改操作
查:find(), count()
#include
#include
#include
int main(){
std::unordered_map<std::string, int> hashMap;
// 增
hashMap["apple"] = 5;
hashMap.insert(std::make_pair("banana", 3));
// 删
hashMap.erase("banana"); // hashMap.clear()清空
// 改
hashMap["apple"] = 4 // 改
// 查
if (hashMap.find("apple") != hashMap.end())
std::cout << "键apple存在" << std::endl;
if (hashMap.count("apple") > 0) // 1则有,0则无
std::cout << "键apple存在" << std::endl;
return 0;
}
放入哈希表的东西,如果是基础类型,内部按值传递,内存占用为这个值所需空间大小
放入哈希表的东西,如果是自定义类型,内部按引用传递,内存占用为指针所需大小
使用有序表,可简单将其理解为一种有序的集合结构,key值不能重复
单键用std::set
;键-值对用std::map
。增删改查API跟哈希相同
红黑树、AVL树、size-balance-tree和跳表等都属于有序表结构,只是底层具体实现不同
基础类型:值传递。自定义类型:引用传递
不管什么底层实现,只要是有序表。固定时间复杂度 O ( l o g N ) O(logN) O(logN)
只要是有序表,都有固定功能:
insert(key, value)
将一条记录加入表中,或者将key的记录更新成valueerase()
删除一条记录begin()
获取key最小的一条记录lower_bound()
获取顺序表中>=给定key值的第一条记录的迭代器upper_bound()
获取顺序表中>给定key值的第一条记录的迭代器find()
获取指定key值的记录的迭代器,如果不等于end()则有效at()
获取指定key值对应的值,会进行边界检查,如过形参超过边界,抛出std::out_of_range类型异常#include
#include
#include
void main(){
std::map<int,string> myMap;
for (int i = 0; i < 10; i++){
myMap.insert(std::pair(i, "我是" + i);
}
string str = myMap.begin()->first; // 获取key值最小的记录(std::pair类型)
myMap.erase(2); // 删除key为2的记录
auto it = myMap.lower_bound(8); // key>=8的第一个记录的迭代器
auto it = myMap.upper_bound(8); // key>8的第一个记录的迭代器
auto it = myMap.--upper_bound(8); // <=8的最后一个元素,即>8的第一个元素的前一个位置,注意这里千万不能后自减,因为返回值是临时变量
// 使用find() 查找指定的键,因为2已经删除了,会找不到
auto it = myMap.find(2);
if (it != myMap.end()) {
std::cout << "Record found: Key = " << it->first << ", Value = " << it->second << std::endl;
} else {
std::cout << "Record not found." << std::endl;
}
// 使用at()获取指定key的对应的值
try {
int value = myMap.at(key);
std::cout << "Record found: Key = " << key << ", Value = " << value << std::endl;
} catch (const std::out_of_range& e) {
std::cout << "Record not found." << std::endl;
}
}
如果顺序表存放自定义类型,需要在类中重载<操作符。必须是常函数并且形参用const修饰。也可以在创建map对象时直接传入一个用于比较的函数的指针
#include
#include
#include
class Person {
public:
Person(const std::string& n, int a) : name(n), age(a) {}
std::string name;
int age;
// 重载<运算符,注意常函数和常形参
bool operator<(const Person& other) const {
return age < other.age;
}
};
// 自定义类型比较方式(通过函数对象,也可以通过普通函数)
struct CompareByName {
bool operator()(const Person& p1, const Person& p2) const {
return p1.name < p2.name;
}
};
int main() {
std::map<Person, int> ageMap;
std::map<Person, int, CompareByName> nameMap; // 注意在<>里面,类型名后面不要带(),如果是std::find_if()这种操作,在传入谓词时函数对象时,则该对象需要带()
Person p1("Alice", 25);
Person p2("Bob", 30);
// 使用默认的比较函数(按照年龄排序)
ageMap[p1] = 25;
ageMap[p2] = 30;
// 使用自定义的比较函数(按照姓名排序)
nameMap[p1] = 25;
nameMap[p2] = 30;
for (const auto& entry : ageMap) {
std::cout << "Age: " << entry.first.age << ", Value: " << entry.second << std::endl;
}
for (const auto& entry : nameMap) {
std::cout << "Name: " << entry.first.name << ", Value: " << entry.second << std::endl;
}
return 0;
}
```
试了一下在VS2022中,有下面这种规则,应该是函数对象作谓词如果放在<>中,不加括号,如果放在()中,则需要带()
std::find_if(it1, it2, IsOdd()); // 这里如果IsOdd是个类,内部重载了()运算符,必须加括号
std::mapnameMap; // 不能加括号
没啥好说的很简单
#include
class ListNode {
public:
ListNode(int x) : val(x), next(nullptr) {}
int val;
ListNode* next;
};
ListNode* reverseList(ListNode* pHead) {
ListNode* pPrev = nullptr;
ListNode* pNext = nullptr;
while (pHead != nullptr) { // pHead即为当前节点
// 先断后连原则
pNext = pHead->next;// 断:断之前保存下个节点的指针(否则下个节点就丢失了)
pHead->next = pPrev;// 连:next连到新的节点上(上一个节点)
// 更新当前节点,更新之前保存上一个节点的地址(否则上个节点就丢失了)
pPrev = pHead;
pHead = pNext;
}
return pPrev;
}
void printList(ListNode* pHead) {
ListNode* curNode = pHead;
while (curNode != nullptr){
std::cout << curNode->val << " -> ";
curNode = curNode->next;
}
std::cout << "nullptr" << std::endl;
}
int main()
{
ListNode* pHead = nullptr;
ListNode* pTail = nullptr;
for (int i = 1; i < 11; i++) {
if (!pHead) {
pHead = new ListNode(i);
pTail = pHead;
}
else {
pTail->next = new ListNode(i);
pTail = pTail->next;
}
}
std::cout << "原始链表:";
printList(pHead);
pHead = reverseList(pHead);
std::cout << "反转后的链表:";
printList(pHead);
// 释放堆区空间
ListNode* p;
while (pReversedHead){
p = pReversedHead;
pReversedHead = pReversedHead->next;
delete p;
}
std::cin.get();
return 0;
}
class ListNode{
public:
ListNode(int x) : val(x), prev(nullptr), next(nullptr) {}
int val;
ListNode* prev;
ListNode* next;
};
ListNode* reverseDoubleList(ListNode* pHead) {
ListNode* pPrev = nullptr;
ListNode* pNext = nullptr;
while (pHead)
{
// 交换节点的next prev指针变量。先断后连,断前先暂存
pNext = pHead->next;
pHead->next = pHead->prev;
pHead->prev = pNext;
// 必须有pPrev这个临时变量用来存放前一个节点的地址,因为循环结束后
// pHead和pNext都为nullptr,如果不存放前一个节点地址,我们就找不到最后一个节点的地址了
pPrev = pHead;
pHead = pNext;
}
return pPrev;
}
int main()
{
ListNode* pHead = nullptr;
ListNode* p = nullptr;
for (int i = 1; i <= 10; i++){// 构造双向链表
if (!pHead){
pHead = new ListNode(i);
p = pHead;
}
else{
p->next = new ListNode(i);
p->next->prev = p;
p = p->next;
}
}
// 翻转操作...
// 打印输出...
// 释放空间...
}
给定两个有序链表的头指针head1和head2,打印两个链表的公共部分。
【要求】若果两个链表的长度之和为N,时间复杂度要求为O(N),额外空间复杂度为O(1)
思路:两个指针,谁小谁移动,相等就打印,直到某一个指针越界为止
#include
class node {
public:
node(int a) : val(a), next(nullptr) {}
int val;
node* next;
};
void printCommonPart(node* head1, node* head2) {
while (head1 && head2) {
if (head1->val < head2->val) {
head1= head1->next;
}
else if (head1->val > head2->val) {
head2= head2->next;
}
else {
std::cout << head1->val << " ";
head1= head1->next;
head2= head2->next;
}
}
}
int main()
{
node* head1 = new node(1);
node* p1 = head1;
for (int i = 2; i <= 10; i++) {
p1->next = new node(i);
p1 = p1->next;
}
node* head2 = new node(2);
head2->next = new node(4);
head2->next->next = new node(6);
printCommonPart(head1, head2);
std::cin.get();
return 0;
}
(1)笔试,不用太在意空间复杂度,一切为了时间复杂度(进面即可)
(2)面试,时间复杂度依然在第一位,但一定要找到空间最省的方法(靠这个提升竞争力)
重要技巧:
下面是一些题型举例
【题目】给定一个单链表的头结点head,请判断该链表是否为回文结构
【例子】1->2->1
,返回true;1->2->2->1
,返回true;1->2->3
,返回false。
【要求】如果链表长度为N,时间复杂度要求O(N),空间复杂度O(1)。
笔试做法(空间复杂度O(N)):搞个栈,遍历链表,全部压入栈内,然后出栈的顺序就是逆序了,挨个与链表每个节点比较,全等则是回文。
#include
class Node {
public:
Node(int x) : val(x), next(nullptr) {}
int val;
Node* next;
};
// 用额外N空间,判断是否回文
bool isPalindrome1(Node* head) {
std::stack<Node> myStack;
Node* p = head;
// 把链表所有节点压入栈
while (p) {
myStack.push(*p);
p = p->next;
}
// 出栈同时,判断是否相等
while (head) {
if (head->val != myStack.top().val) {
return false;
}
head = head->next;
}
return true;
}
稍微优化它的空间复杂度的做法:让链表的后面一半元素入栈,空间复杂度降低到O(N/2),如何实现?—— 快慢指针
快指针走2步,慢指针走1步,当快指针走到尾节点,慢指针刚好走一半
快慢指针一定要自己写代码,熟练以下几种控制慢指针位置的边界情况。当快指针走完时
中点、中点的前一个位置、中点的后一个位置
前一个位置、前两个位置、后一个位置
熟练是为了在笔试或面试的时候节省时间
快指针可以走的条件(经过大量实践总结出来的,最好的写法)
fast->next != nullptr && fast->next->next != nullptr
最终快指针必然停在尾结点(奇数个) 或 尾结点前一个节点(偶数个)。
根据fast指针位置,可以判断链表结点个数奇偶性
fast->next == nullptr
fast->next != nullptr
根据奇偶性,可以知道慢指针的精确位置
N/2+1
N/2
拿到慢指针精确位置,就可以拿到其相邻的节点位置
后一个:slow->next
,后两个:slow->next-next
前一个:额外申请一个指针prev,在慢指针往后走之前,记录slow的指向
while (fast->next != nullptr && fast->next->next != nullptr{
Node* prev = slow;
fast = fast->next->next;
slow = slow->next;
}
/
是整除
bool isPalindrome2(Node* head) {
if (!head || !head->next) {
return true;
}
Node* right = head->next; // 慢指针
Node* cur = head; // 快指针
// 找到链表的右半部分的起始指针
while (cur->next && cur->next->next) {
right = head->next;
cur = cur->next->next;
}
// 入栈
std::stack<Node> stack;
while (right) {
stack.push(*right);
right = right->next;
}
// 出栈
while (!stack.empty()) {
if (head->val != stack.top().val)
return false;
head = head->next;
}
return true;
}
步骤: