链表无疑是数据结构中的基础,所以,有必要进行一些总结。链表分为单链表、双链表、循环链表等,在此,我们以单链表为例,简单了解一下链表的基本实现,至于双链表、循环链表等,实现可能比单链表要复杂,但基本原理一致。
链表的构造
链表主要由节点构成,节点结构如下:
typedef struct Node
{
int data;
struct Node *next;
} NODE, *PNODE;
data是实际存储数据,当然,我们也可以存储比int型更复杂的数据,next则是节点指针,每个节点的next指针指向下一个节点,从而形成了如下结构:
如果是循环链表,则是如下形式:
这里面需要注意头节点,头节点并不保存有实际意义的数据,主要用来找到链表入口,方便链表操作。需要注意的是,虽然头节点没有有效数据,但确实不可缺少的,因为在删除操作时,如果没有头节点,而只使用一个指针指向第一个节点的话,一旦该节点删除,那么整个链表就会丢失。
链表的增删改查
/*
* @Descripttion: 链表实现
* @version:
* @Author: sueRimn
* @Date: 2021-01-10 10:23:48
* @LastEditors: sueRimn
* @LastEditTime: 2021-01-10 17:14:01
*/
#include <malloc.h>
#include <iostream>
typedef struct NODE
{
int data;
struct NODE *next;
} Node, *pNode;
void Print_Link(pNode head)
{
if (head == nullptr || head->next == nullptr)
{
printf("The link isempty!\n");
return;
}
pNode p = head->next; //指向第一个节点
while (p != nullptr)
{
printf("%d\n", p->data);
p = p->next;
}
}
/**
* @name: 创建链表
* @test: test font
* @msg:
* @param {*}
* @return {*}
*/
pNode CreateLink()
{
pNode head = nullptr;
head = (pNode)malloc(sizeof(Node));
if (head == nullptr)
{
printf("malloc failed!");
return head;
}
head->next = nullptr;
return head;
}
/**
* @name: 前插
* @test: test font
* @msg:
* @param {*}
* @return {*}
*/
bool insert_head(pNode head, int value)
{
if (head == nullptr)
return false;
pNode newNode = (pNode)malloc(sizeof(Node));
if (newNode == nullptr)
return false;
newNode->data = value;
newNode->next = head->next;
head->next = newNode;
return true;
}
/**
* @name: 后插
* @test: test font
* @msg:
* @param {*}
* @return {*}
*/
bool insert_tail(pNode head, int value)
{
if (head == nullptr)
return false;
pNode p = head;
while (p != nullptr && p->next != nullptr)
{
p = p->next;
}
//循环结束后找到最后一个节点
pNode newNode = (pNode)malloc(sizeof(Node));
if (newNode == nullptr)
{
printf("malloc error!\n");
return false;
}
newNode->data = value;
newNode->next = nullptr;
p->next = newNode;
return true;
}
/**
* @name: 前删
* @test: test font
* @msg:
* @param {*}
* @return {*}
*/
void delete_head(pNode head)
{
if (head == nullptr || head->next == nullptr)
return;
pNode p = head->next; //要删除的节点
head->next = p->next;
free(p);
p = nullptr;
}
/**
* @name: 后删
* @test: test font
* @msg:
* @param {pNode} head
* @return {*}
*/
void delete_tail(pNode head)
{
if(head == nullptr || head->next == nullptr)
return;
pNode p = head;
pNode prev = head; //用于记录最后一个节点前面的一个节点
while (p != nullptr && p->next != nullptr)
{
prev = p;
p = p->next;
}
//循环结束后找到最后一个节点
prev->next = nullptr;
free(p);
p = nullptr;
}
void clear_link(pNode head)
{
if (head == nullptr || head->next == nullptr)
return;
while (head->next != nullptr)
{
delete_head(head);
}
}
int main()
{
pNode head = CreateLink();
//前插
for (int i = 1; i < 5; i++)
{
insert_head(head, i);
}
printf("insert head:\n");
Print_Link(head);
//前删
delete_head(head);
printf("delete head:\n");
Print_Link(head);
//清空
clear_link(head);
printf("clear link:\n");
Print_Link(head);
//后插
for (int i = 1; i < 5; i++)
{
insert_tail(head, i);
}
printf("insert tail:\n");
Print_Link(head);
//后删
delete_tail(head);
printf("delete tail:\n");
Print_Link(head);
}
运行后,结果如下:
insert head:
4
3
2
1
delete head:
3
2
1
clear link:
The link isempty!
insert tail:
1
2
3
4
delete tail:
1
2
3
链表相关算法
链表反转
链表反转示意图可以用下图表示:
总结如下:
- 默认cur指向链表第一个节点,prev指向NULL;
- 循环时,cur的next指针指向prev节点,prev节点指向cur节点,cur指针指向下一个节点。
- 依次循环,最终链表彻底反转。
代码实现如下:
/**
1. @function: 链表反转
2. @param {pNode} head
3. @return {*}
*/
void reverse_Link(pNode head)
{
if (head == nullptr || head->next == nullptr)
return;
pNode cur = head->next;
pNode prev = nullptr;
while (cur != nullptr)
{
pNode node = cur->next;
cur->next = prev;
prev = cur;
cur = node;
}
//循环之后,prev成为第一个链表节点
head->next = prev;
}
链表相交
两个单链表,判断其是否相交,如果相交,获取交点。
首先要明确,如果两个链表相交,那么,其一定是如下形式的:
因为如果两链表如果相交于点D,由于链表通过next指针指向下一个节点,那么点D后的链表段两个链表是共有的。
如果有相交的结点D的话,每条链的头结点先走完自己的链表长度,然后回头走另外的一条链表,那么两结点一定为相交于D点,因为这时每个头结点走的距离是一样的,都是 AD + BD + DC,而他们每次又都是前进1,所以距离相同,速度又相同,固然一定会在相同的时间走到相同的结点上,即D点。
void Node getIntersectionNode(Node headA, Node headB)
{
if (headA == nullptr || headB == nullptr)
return nullptr;
Node p1 = headA;
Node p2 = headB;
while (p1 != p2)
{
if (p1 == nullptr)
p1 = headB;
else
p1 = p1.next;
if (p2 == nullptr)
p2 = headA;
else
p2 = p2.next;
}
return p1;
}
获取链表中倒数第k个元素
假定有total个节点,那么获取其倒数第k个节点的元素。
可以这样分析:如果是倒数第k个节点,正数是第几个节点?由于一共有total个节点,倒数第k个节点的左侧那么一定有total-k个节点,那么倒数第k个节点正数也就是total-k+1个节点。即total-(k-1)。
既然已经知道了是正数第total-(k-1),我们的当然可以直接遍历到第total-(k-1)个节点去,但是存在问题:链表我们只有其head节点,而事前并不知道其total是多少,除非我们先遍历一次,获取total数值,那么时间复杂度也就是O(2n),简化即O(n)。
但这里介绍一种更简单的办法,只需遍历一次,即双指针法。
首先定义两个指针prev、behind,则步骤如下:
- prev指针首先从head指针起,往前遍历k-1次。
- prev指针遍历k-1次后,behind指针指向head节点,并开始与prev指针一同遍历,直到prev指针的next为空。
分析:当prev指针遍历k-1次后,也就是到达正数第k-1个节点,那么它还需要total-(k-1)次遍历才能到达第total个节点,如果behind指针和其一同遍历,则behind最终到达正数第total-(k-1)个节点,而正如前面分析,正数第total-(k-1)个节点,实际也就是倒数第k个节点。
/**
* @function: 寻找倒数第k个节点
* @param {pNode} head
* @param {int} k
* @return {int}
*/
int find_inverseK_node(pNode head, int k)
{
if (head == nullptr || head->next == nullptr)
return;
pNode prev = head;
pNode behind = head;
int step = 0;
while (prev->next != nullptr)
{
prev = prev->next;
step++;
if (step > (k - 1)) //prev先行k-1步,然后behind指针也开始运动
behind++;
}
//k有可能大于了total,那么behind就没机会运动了
if (behind == head)
return -1;
return behind->data;
}