目录
一、链表的基础操作
链表是结构体变量与结构体变量之间通过指针连在一起,形成的一张表。
在下列代码中,将三个结构体变量通过指针形成了如图所示的链表。
#include <stdio.h>
struct Node
{
int data; //数据域
struct Node* next; //指针域
}
int main()
{
//定义了三个结构体变量
struct Node Node1 = {1,NULL};
struct Node Node2 = {2,NULL};
struct Node Node3 = {3,NULL};
//创建静态链表
Node1.next = &Node2;
Node2.next = &Node3;
}
这是最简单的静态链表,一般我们不用。我们一般使用动态链表,更实用,那么如何动态地创建链表呢?大致分为如下几步:
1、创建链表
创建一个表头表示整个链表。我们一般使用采用某种方法使指针成为一个结构体变量,让其成为表头,整个过程需要动态内存申请,如图所示:
代码如下:
struct Node* createList()
{
struct Node* headNode = (struct Node*)malloc(sizeof(struct Node));
//headNode 成为了结构体变量
//变量使用前必须被初始化
//headNode -> data = 1; //一般数据域可以不初始化,因为后续代码中data可能是结构体类型的
headNode -> next =NULL;
return headNode;
}
注意:需要申请的内存大小就是结构体的大小。一定要在malloc前进行强制类型转换。(struct LinkList*)
2、创建节点
创建节点和创建头结点的流程基本一致,不过多了一个data。
struct Node* createList(int data)
{
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode -> data = data; //一般数据域初始化为形参
newdNode -> next =NULL;
return newNode;
}
3、插入节点
插入节点分为头插法和尾插法:
(1)头插法:
将新节点插入在头节点后面,如图:
代码如下:
参数:插入的是哪个链表,插入的是什么数据
void insertNode(struct Node* headNode,int data)
{
struct Node*newNode = createNode(data); //使用了上面的函数创建新的节点,即要插入的节点
newNode -> next = headNode -> next; //新的节点要指向表头的下一个节点
haed ->next = newNode; //表头指向新插入的节点
}
(2)尾插法:
将新节点插入在链表的尾部,如图:
代码如下:
void insertNode(struct Node* headNode,int data)
{
struct Node* newNode = createNode(data); //使用了上面的函数创建新的节点,即要插入的节点
while(headNode->next) //只要头结点后面还有节点,就可以进行尾插法
{
headNode = headNode -> next; //一直往后移,找到链表中最后一个节点
}
headNode->next = newNode;//在最后一个节点的后面插新节点
}
4、删除节点
一般用到较多的是指定位置删除,在下图中我们删除节点posNode:
代码如下:
void deleteNode(struct Node* headNode,int posData)
{
struct Node* posNode = headNode -> next; //posNode只能从表头的下一个节点开始找
struct Node* posNodeFront = headNode; //posNodeFront要从posNode的前一个开始找,即headNode开始
if(posNode) //只要posNode不为空,就可以删除
{
while(posNode -> data == posData) //posNode现在指向的节点数据是我们要删除的指定数据
{
if(posNode == headNode)//如果找到的节点是头节点
{
headNode = posNode->next; //删除头结点,让posNode->next成为头结点
}
else//如果找到的节点是普通节点
{
posNodeFront -> next = posNode->next; //跳过了posNode这个节点,意味着删除了它
}
else //没有找到咱们要删除的节点
{
printf("没有找到您要删除的节点\n");
}
free(posNode); //释放被删除节点所占用的空间
}
}
}
5、遍历打印链表
打印链表主要用到的就是遍历,使用一个pMove节点挨个挨个打印,如图:
代码如下:
void printList(struct Node* headNode) //打印不需要返回值
{
struct Node* pMove = headNode -> next; //pMove指针从第二个节点开始打印
while(pMove) //当pMove节点不为空的时候就可以打印
{
printf("%d",pMove -> data); //打印指针的数据域
pMove = pMove->next; //指针指向下一个节点
}
printf("\n");
}
上面五个就是链表中最基本的操作,还有一个操作都是在这些基础操作上演变优化而来,下面也记录一下我在刷题过程中遇到的几种典型题吧。
二、链表常见题型
1、反转链表
将L指针指向链表中第二个节点,再将首元结点指向空,断开与后面链表的连接,然后指针pr指向首元结点,保存下首元结点,如下图:
然后将第二个节点放到首元结点之前,使用 pHead->next=pr 实现,然后将pr指向反转后新的子链表的第一个节点,之后将pHead指向还未反转的子链表的首节点,继续反转操作:
下图是反转一次后形成的效果图,之后就重复上面的操作,就可以将所有节点都反转过来:
功能函数如下:
struct rollNode* ReverseList(struct Node* pHead )
{
struct ListNode *Pr=NULL; //前段链表
struct ListNode *L=NULL; //存放后段链表
if(pHead!=NULL) //链表要不为空
{
while(pHead!=NULL) //循环
{
L=pHead->next; //后段链表存入L中
pHead->next=Pr; //将头结点指针指向空
Pr=pHead; //将前段链表存入Pr中
pHead=L; //将还未排序的后段链表放回去继续排序
}
pHead=Pr; //排好序的数据存放在pr中,将其放入phead中
}
2、反转链表内指定区间
反转指定区间首先要找到指定区间在哪里,加入区间在【m,n】的话,通过指针pre遍历寻找,找到后使用一个start指针指向要反转区间的首节点,然后利用头插法进行反转,头插法上面已经有解释,这里就不再详述,如下图:
功能函数:
struct ListNode* reverseBetween(struct ListNode* head, int m, int n )
{
struct ListNode* dummy = (struct ListNode*) malloc( sizeof(struct ListNode) );
dummy->next = head; //创建一个头结点,指向链表的头部
struct ListNode* pre = dummy; //先定义一个节点pre,指向头结点
//使用pre遍历,找到指定要反转的区间,用pre指向这个区间的前一个节点
for (int i = 1; i < m; ++i)
{
pre = pre->next;
}
struct ListNode* start = pre ->next; //start指向这个区间的第一个节点,也就是反转后链表的尾节点
//头插法反转
for (int i = 0; i < n - m; i++)
{
struct ListNode* tmp = start->next; //定义tmp,指向区间内第一个节点后的部分
start->next = tmp->next; //跳过tmp节点
tmp->next = pre->next; //将tmp节点插入到pre与start节点之间
pre->next = tmp; //将pre与tmp连接上
}
return dummy->next; //反转好的链表是存入在dummy中的,去掉头结点,也就是dummy->next
}
3、链表中的节点每k个一组翻转
如果链表中有n个数据,那么一共反转的次数就是n/k,我尝试使用头插法进行反转,但是在反转次数达到一定程度后,使用头插法比较麻烦,因此这里使用直接将要反转链表后面的节点放到前面的方法,以下图为例,首先创建三个指针,p指向首元结点,x和c都指向第二个节点,然后将第一个和第二个节点反转,得到两段子链表,同时将x指向下一个节点,也就是还未反转的子链表的首节点:
如果k>2的话,就继续反转下一个节点,如下图:
重复反转完第一组的节点后,此时形成了已完成反转的节点和未完成反转的节点,此时要继续反转后面几组节点,并且将各组反转好的节点首尾连接起来,就先创建三个指针p0,p1,p1,p0指向还下一组要进行反转的首节点,p1指向每次反转完成后的子链表的首节点,作为连接头部,p2指向已经反转好的子链表的最后一个节点,作为连接尾部,然后使用p2->next = p1 将已经完成反转的前一组子链表的尾部和后一组子链表的头部连接起来,最后还是不需要反转的剩余部分,用p指向它,将连接好的链表与其相连,如下图:
功能函数如下:
struct ListNode* reverseKGroup(struct ListNode* head, int k ) {
// write code here
int n = 0; //节点数
int i;
int m; //m限制头插节点的个数,每反转一次需要头插k-1个节点
//保证链表中有数据并且能够反转
if ( head == NULL || k <= 1 || head->next == NULL) {
return head;
}
struct ListNode* pr = head; //创建头指针,用来求链表长度
//求一下链表的长度
while (pr != NULL) {
pr = pr->next;
n++;
}
if (n < k) {
return head;
}
struct ListNode* p0 = NULL;
struct ListNode* p1 = NULL;
struct ListNode* p2 = NULL;
struct ListNode* c ; //指向头结点的下一个节点,用来插入
struct ListNode* x; //指向头结点的下一个节点,主要是用来定位的
struct ListNode* p; //指向头结点,用来固定插入的地方
c = head;
x = c->next;
p = c;
c = x;
for (i = 1; i <= (n / k); i++)
{ //一共要反转多少次
p0 = p; //存入还未反转的完整链表,第二次反转后会存入要反转但是还未反转的子链表,p0={1,2,3,4,5} ,p0={1,2,1,2,1,2...},p0={3,4,5},p0={3,4,3,4...}
for (m = 1 ; m < k ; m++) { //一组一组的反转,一组有k个数据,要头插k-1次
//每次反转需要头插k-1个节点,m=2-1=1
x = c->next; //将指针x往后移一位
c->next = p; //将c插入到head前面
p = c; //将p后移,保证每次插入的节点都在p的前面
c = x; //将c往后移一位
}
p1 = p; //存入每次反转后的子链表,p1={2,1,2,1...},p1={4,3,4,3...}
if ( i == 1 ) { //如果只反转了一次,那么结果就是p1
head = p1;
}
if ( i >= 2) { //如果反转了多次,就要将几次反转的子链表首尾结合在一起
p2->next = p1; //p2={1,2,1,2...}会变成p2={1,4,3,4,3...}首尾相连
p0->next = NULL; //将没有连上的链表节点连起来,避免出现链表断裂的情况,p0: {3}, p1: {4,3}, p2: {1,4,3}
}
p2 = p0; //把第一次未反转的子链表保存下来,以便和后续子链表相连,p2={1,2,1,2...},第二次: p0: {3},p1: {4,3},p2: {3},head: {2,1,4,3}
x = c->next; //继续后移,继续反转
p = c;
c = x;
}
p2->next = p; //反转完成之后,将p指向剩余的链表的首节点,通过p2将反转过的和没有反转过的部分连接起来p0: {3,5},p1: {4,3,5},p2: {3,5},head: {2,1,4,3,5}
return head;
}