【算法C++实现】4、链表(包括哈希表、顺序表的简单介绍)

文章目录

  • 1 相关容器介绍
    • 1.1 哈希表简单使用
    • 1.2 有序表的简单使用
  • 2 链表的一些简单题
    • 2.1 单向链表反转
    • 2.2 双向链表反转
    • 2.3 打印两个有序链表的公共部分
  • 3 面试链表题型方法论
    • 3.1 题:判断一个链表是否是回文结构
      • 3.1.1 快慢指针介绍
      • 3.1.2 题:判断回文结构(额外空间优化成n/2)
      • 3.1.3 题:判断回文结构(额外空间O(1))

1 相关容器介绍

1.1 哈希表简单使用

这一节,并不要求掌握内部结构,只需要在刷题需要时会使用即可。

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;
      }
      
  • 放入哈希表的东西,如果是基础类型,内部按值传递,内存占用为这个值所需空间大小

  • 放入哈希表的东西,如果是自定义类型,内部按引用传递,内存占用为指针所需大小

1.2 有序表的简单使用

  • 使用有序表,可简单将其理解为一种有序的集合结构,key值不能重复

  • 单键std::set键-值对std::map。增删改查API跟哈希相同

  • 红黑树、AVL树、size-balance-tree和跳表等都属于有序表结构,只是底层具体实现不同

  • 基础类型:值传递。自定义类型:引用传递

  • 不管什么底层实现,只要是有序表。固定时间复杂度 O ( l o g N ) O(logN) O(logN)

  • 只要是有序表,都有固定功能:

    • insert(key, value)将一条记录加入表中,或者将key的记录更新成value
    • erase()删除一条记录
    • 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::map nameMap; // 不能加括号


2 链表的一些简单题

2.1 单向链表反转

没啥好说的很简单

#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;
}

2.2 双向链表反转

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;
		}
	}
	// 翻转操作...
	// 打印输出...
	// 释放空间...
}

2.3 打印两个有序链表的公共部分

给定两个有序链表的头指针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;
}	

3 面试链表题型方法论

(1)笔试,不用太在意空间复杂度,一切为了时间复杂度(进面即可)
(2)面试,时间复杂度依然在第一位,但一定要找到空间最省的方法(靠这个提升竞争力)

重要技巧:

  • 额外数据结构记录(哈希表等容器)
  • 快慢指针

下面是一些题型举例

3.1 题:判断一个链表是否是回文结构

【题目】给定一个单链表的头结点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),如何实现?—— 快慢指针

3.1.1 快慢指针介绍

快指针走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;
    }
    

/ 是整除


3.1.2 题:判断回文结构(额外空间优化成n/2)

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;
}

3.1.3 题:判断回文结构(额外空间O(1))

步骤:

  • 快慢指针找到中点,把中点next指针指向nullptr
  • 从中点下一个节点开始反转链表
  • 然后用head指针从前往后,用fast指针从后往前遍历,判断每个数是否相等,直到有一个指针为空。(不管是奇数个偶数个都一样)

你可能感兴趣的:(刷题C++实现,C++,c++,链表,开发语言)