单链表
目录
概念
顺序表可以随时存取表中的任意一个元素,它的存储位置可以用一个简单直观的公式表示,但插入和删除操作需要移动大量元素。链式存储线性表时,不需要使用地址的连续存储单元,即不要求逻辑上相邻元素在物理地址上也相邻,它通过“链”建立起数据元素之间的逻辑关系,因此插入和删除元素操作不需要移动元素,而只需要修改指针,但也会失去顺序表可随机存取的优点
基本操作有:
1、创建单链表
2、遍历单链表
3、插入单链表元素
4、单链表删除元素
5、判断单链表是否为空
6、单链表的长度
7、查找单链表元素
代码
#include <iostream> using namespace std; struct List { int val; struct List* next; }; void Init(struct List* L) { //创建链表 int cur; cin >> cur; while (cur != -1) { struct List* ptr = (struct List*)malloc(sizeof(struct List)); ptr->val = cur; ptr->next = NULL; L->next = ptr; L = L->next; cin >> cur; } } void Show(struct List* L) { //遍历链表值 cout << "链表遍历:" ; while (L->next) { cout << L->next->val << " "; L = L->next; } cout << endl; } //在第K个位置前插入data元素,最后链表 的第K个位置就是data void InsertList (struct List* L, int k, int data) { struct List* pre = NULL; //存储第K-1个元素的值 struct List* ptr = (struct List*)malloc(sizeof(struct List)); //申请空间 ptr->val = data; ptr->next = NULL; while (k && L->next) { //查找到放置data元素的位置 pre = L; L = L->next; k --; } if (k > 0) { //如果K > 0 直接插到链表的表尾 L->next = ptr; L = L->next; }else { pre->next = ptr; //链接链表 ptr->next = L; } } int lengthList (struct List* L) { //求链表长度 int len = 0; while (L->next) { len ++; L = L->next; } return len; } void DeleteList (struct List* L, int x) { //删除值为x的结点(链表无重复值) if (lengthList(L) <= 0) { cout << "表空,没有元素可删除" << endl; return; } struct List* ptr = L->next; struct List* pre = L; //记录ptr的前一个位置的结点 while (ptr) { if (ptr->val == x) { pre->next = ptr->next; //把x值的结点的前一个结点的next指向ptr的next结点 free(ptr); //释放空间 return; } pre = ptr; ptr = pre->next; } } void DeleteList_Position(struct List* L, int k) { //删除第K个位置的结点 if (lengthList(L) <= 0) { cout << "表空,没有元素可删除" << endl; return; } struct List* ptr = L->next; struct List* pre = L; //记录ptr的前一个位置的结点 k = k - 1; //因为如果k = 1,直接用pre->next = ptr->next就把ptr删掉了,所以要减1 while (k-- && ptr) { pre = ptr; ptr = ptr->next; } if (ptr == NULL || k > 0) { cout << "要删除的位置不存在" << endl; }else { pre->next = ptr->next; //删除ptr结点 free(ptr); //释放空间 } } bool IsEmptyList(struct List* L) { //判断链表是否为空 if (L->next == NULL) { return true; }else { return false; } } int GetElemList(struct List* L, int i) { //返回第i个位置的值 struct List* ptr = L; int k = i; //标记i的值,以防不存在输出显示 while (i > 0 && ptr->next) { ptr = ptr->next; i --; } if (i == 0 && ptr != NULL) { //当i == 0 和 ptr 不为空代表找到了第i个位置的元素 return ptr->val; }else { cout << "第" << k << "个位置不存在" << endl; return -1; } } void ClearList(struct List* L) { //清空链表 struct List* ptr = L; if (lengthList(L) > 0) { while (ptr->next) { struct List* temp = ptr->next; ptr->next = ptr->next->next; free(temp); //释放空间 } } } int main() { struct List* head = (struct List*)malloc(sizeof(struct List)); //头结点(不存值) head->next = NULL; while(1) { cout << "**********************************************" << endl; cout << "* 1、创建单链表(以-1结束) 2、打印单链表 *" << endl; cout << "* 3、插入单链表 4、单链表删除 *" << endl; cout << "* 5、判断是否为空 6、单链表长度 *" << endl; cout << "* 7、查找 8、退出 *" << endl; cout << "**********************************************" << endl; int k; cout<<"请输入你的选择:"; cin>>k; switch(k) { case 1: Init(head); //创建单链表 system("pause"); system("cls"); continue; case 2: Show(head); //遍历单链表 system("pause"); system("cls"); continue; case 3: int i, data; cout << "请输入要插入的位置和值:"; cin >> i; cin >> data; InsertList(head, i, data); Show(head); system("pause"); system("cls"); continue; case 4: int x; cout << "请输入要删除的值: "; cin >> x; DeleteList(head, x); //删除链表中值为x的结点(链表值无重复) system("pause"); system("cls"); continue; case 5: if (IsEmptyList(head)) cout << "链表是空链表!" << endl; else cout << "链表不空!" << endl; system("pause"); system("cls"); continue; case 6: cout << "链表的长度为: " << lengthList(head) << endl; system("pause"); system("cls"); continue; case 7: int n; cout << "请输入要查找的位置: "; cin >> n; if (GetElemList(head, n) != -1) cout << "第" << n << "个位置的值为: " << GetElemList(head, n) << endl; system("pause"); system("cls"); continue; case 8: break; default: cout << "请输入正确的选项!!!" << endl; system("pause"); system("cls"); continue; } system("cls"); break; } system("pause"); return 0; }
算法考题
快慢指针找链表中间位置
题目描述
给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例 1:
输入:[1,2,3,4,5] 输出:此列表中的结点 3 (序列化形式:[3,4,5]) 返回的结点值为 3 。
(测评系统对该结点序列化表述是 [3,4,5])。 注意,我们返回了一个 ListNode 类型的对象 ans,这样: ans.val =
3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next =NULL.
示例 2:
输入:[1,2,3,4,5,6] 输出:此列表中的结点 4 (序列化形式:[4,5,6]) 由于该列表有两个中间结点,值分别为 3 和
4,我们返回第二个结点。
解析
快慢指针,慢指针一次走一步,快指针一次走两步,快指针下一跳为Null时,慢指针再走一步就是中间点
代码
struct ListNode* middleNode(struct ListNode* head){//快慢指针 struct ListNode* fast=head; struct ListNode* slow=head; while(fast&&fast->next)//fast走两步,slow走一步,判断fast->next!=NULL为了防止fast->next->next // 非法访问 { fast=fast->next->next; slow=slow->next; } return slow; }
快慢指针找带环链表入口
题目
leetcode 142
给定一个链表,返回链表开始入环的第一个节点。若链表无环,返回 null
示例
使用整数 pos 表示链表尾连接到链表中的位置(索引从 0 开始)。
输入:head = [3, 2, 0, -4],pos = 1
输出:返回索引为 1 的链表节点。
解释:链表中有一个环,其尾部连接到第二个节点。
输入:head = [1],pos = -1
输出:返回 null
解释:链表中没有环。
提示
pos 不作为参数进行传递,仅标识链表的实际情况。
0 <= 链表节点数 <= 10^4
10^-5 <= Node.val <= 10^5
pos 为 -1 或者链表中的一个有效索引。
解析
题目解决分为两步走:
- 判断是否有环。
- 若有环,找入口,找到结束;若无环,结束。
第一步:判断是否有环。
做法同 LeetCode 141。
判断是否有环,一般使用快慢指针。
快慢指针,顾名思义,是使用速度不同的指针(可用在链表、数组、序列等上面),来解决一些问题。
这些问题主要包括:
- 处理环上的问题,比如环形链表、环形数组等。
- 需要知道链表的长度或某个特别位置上的信息的时候。
快慢指针这种算法证明,它们肯定是会相遇的,快的指针一定会追上慢的指针,可以理解成操场上跑步,跑的快的人套圈跑的慢的人。
一般用 fast 定义快指针,用 slow 定义慢指针。
速度不同是指 fast 每次多走几步,slow 少走几步。一般设定的都是 fast 走 2 步,slow 走 1 步。
当然设置成别的整数也是可以的,比如 fast 走 3 步,slow 走 1 步。
第二步:找入口。
假设:
- 从头节点到入口的距离为 ab。
- 从入口到相遇点的距离为 bc。
- 从相遇点到入口的距离为 cb。
则链表环的长度 = bc + cb。
那么快慢指针相遇时所走过的步数为:
fast_step = ab + bc + n * (bc + cb) 【公式 1】
slow_step = ab + bc 【公式 2】
n 指的是 fast 指针走过的圈数,n 的取值肯定是 n ≥ 1,因为快指针 fast 最少要多跑一圈才能追上慢指针 slow。
又由于快指针每次走 2 步,慢指针每次走 1 步,所以又有:
fast_step = 2 * slow_step 【公式 3】
综合公式 1、公式 2、公式 3,得出:
ab + bc + n * (bc + cb) = 2 * (ab + bc) 【公式 4】
整理得:
ab = (n - 1) * (bc + cb) + cb 【公式 5】
其中 (n - 1) * (bc + cb) 正好是 (n - 1) 个环形的长度。
为了更好的理解公式 5,我们假设 n = 1,也就是快指针 fast 指针走一圈就可以碰到慢指针 slow。
那么公式 5 变成:
ab = cb
你看这个公式好巧妙,ab 是从节点到入口的距离,cb 是从相遇节点到入口的距离,这说明什么呢?
说明同时从头节点和相遇节点出发的两个指针,每次走 1 步,最终会在入口处相遇。
所以如果判断链表是环形链表,“找入口”就成了,在找到快慢指针相遇节点时,设置两个新节点 flag1 和 flag2。
其中 flag1 指向头节点,flag2 指向快慢指针相遇节点,然后每次移动 1 步,直至 flag1 和 flag2 相遇。
代码
ListNode* detectCycle(ListNode* head) { if (head == nullptr) return nullptr; ListNode* fast = head; ListNode* slow = head; while (fast != nullptr && fast->next != nullptr) { fast = fast->next->next; slow = slow->next; if (fast == slow) { ListNode* i = head; ListNode* j = fast; while (i != j) { i = i->next; j = j->next; } return i; } } return nullptr; }
链表逆置
题目描述
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:
输入:head = []
输出:[]
解析
翻转单链表的要点在于保存上一个节点pre,下一个节点next,提前保留好,然后翻转就是now 指向pre,next指向now,然后把next 设置为pre
注意边界情况比如head节点,以及NULL节点。
代码
class Solution { public: ListNode* reverseList(ListNode* head) { ListNode* prev = nullptr; ListNode* curr = head; while (curr) { ListNode* next = curr->next; curr->next = prev; prev = curr; curr = next; } return prev; } };
两个可能带环也可能不带环的链表找交点
题目
给定2个链表,head1,和head2
他们可能有环,可能无环
若两个链表相交,请返回相交的第一个交点,否则返回null
解析
分情况:
- 两个链表都有环
- 一个有环一个没环
- 都没环
都没环的情况:
- 找到两个链表末尾,判断是否地址是否相同,如果相同就是有交点
- 如果有交点,就x = len1 - len2 ,长链表先走x,之后两链表一起走,相遇为交点
一个有环一个没环:
直接返回NULL,因为不可能相交
都有环:
- 一种是在入口之前相交,这种情况找到入口点,然后把入口点当成末尾,走都没环都情况
- 一种是两环相交(有各自的入口),这种情况,两个链表的入口 分别为a,b,a不断遍历next,在重新回到原来位置之前遇到b就是这种情况,然后返回a,b都可以。
代码
class Node{ int value; Node next; public Node(int value){ this.value = value; } } private static Node findCrosserPoint(Node node1,Node node2) { if(node1 == null || node2 == null) { return null; } Node nodeN = node1; Node nodeM = node2; int n = 0; while (nodeN.next != null){ n++; nodeN = nodeN.next; } while (nodeM.next != null){ n--; nodeM = nodeM.next; } if(nodeN != nodeM){ return null; } nodeN = n > 0 ? node1 : node2; nodeM = nodeN == node1 ? node2 : node1; n = Math.abs(n); for (int i = 0; i < n; i++) { nodeN = nodeN.next; } while(nodeN != null){ if(nodeN == nodeM) { break; } nodeN = nodeN.next; nodeM = nodeM.next; } return nodeN; } private static Node isCycle(Node node) { // 思路二。 使用快慢指针,链表有环快慢指针必然会重合,若无环快指针会走到null if(node == null || node.next == null || node.next.next == null){ return null; } Node nodeF = node.next.next; Node nodeS = node.next; while(nodeF != nodeS) { if(nodeF.next == null || nodeF.next.next == null) { return null; } nodeF = nodeF.next.next; nodeS = nodeS.next; } // 找到入口点 nodeF = node; while(nodeF != nodeS) { nodeF = nodeF.next; nodeS = nodeS.next; } return nodeF; } private static Node findCrosser(Node node1,Node node2,Node loop1,Node loop2) { if(node1 == null || node2 == null || loop1 == null || loop2 == null) { return null; } if(loop1 == loop2) { // 第二种情况 --> 这种情况和无环相交类似,只是快节点走到null改为快节点走到环入口点 Node nodeN = node1; Node nodeM = node2; int n = 0; while (nodeN.next != loop1){ n++; nodeN = nodeN.next; } while (nodeM.next != loop2){ n--; nodeM = nodeM.next; } nodeN = n > 0 ? node1 : node2; nodeM = nodeN == node1 ? node2 : node1; n = Math.abs(n); for (int i = 0; i < n; i++) { nodeN = nodeN.next; } while(nodeN != null){ if(nodeN == nodeM) { return nodeN; } nodeN = nodeN.next; nodeM = nodeM.next; } } else { Node node = loop1.next; while(node != loop1) { if(node == loop2){ return node; } node = node.next; } return null; } return null; } private static Node isCrosser(Node node1,Node node2) { Node loop1 = isCycle(node1); Node loop2 = isCycle(node2); if(loop1 == null && loop2 == null) { return findCrosserPoint(node1,node2); } if(loop1 != null && loop2 != null) { return findCrosser(node1,node2,loop1,loop2); } // 一个有环一个无环必然不相交 return null; }