链
可能有很多人已经学习了数据结构,但是还是不那么精通,今天我们一起学习链表。
开始这篇文章之前,代码部分我引用了印度老哥Harsha Suryanarayana的代码,并对学习过程中有问题的点进行描述。
本文章分为6个部分:
1.头部插入一个节点
2.任意位置插入或删除一个节点
3.反转链表之迭代
4.递归打印链表
5.反转链表之递归
6.双向链表
1.头部插入一个节点
代码:
结构体
struct Node
{
int data;
struct Node* next;
};
struct Node* head;
插入函数:
void Insert(int x)
{
struct Node* temp =(strcut Node*)malloc(sizeof(struct Node));
temp->data = x;
temp->next =head;
head =temp;
}
代码解释:
首先在堆上分配空间;
其次进行赋值,并且把头指针的地址传入 temp->next 因为 head 的地址是NULL,那么此时 temp->next 的地址因为是NULL;
最后我们把 temp 的地址传入 头指针 head。
输出函数
void Print()
{
struct Node* temp = head;
while(temp != NULL)
{
printf("%d ",temp->data);
temp = temp->next;
}
printf("\n");
}
代码解释:
首先引用一个临时变量,避免失去头节点;
其次进行值的打印,如果此时 temp 的值不为NULL,那么进行循环、打印。
主函数:
int main()
{
head = NULL;
printf(''需要多少个数字");
int n,i,x;
sxanf("%d",&n);
for(i=0,i<n;i++)
{
printf("输入值");
scanf("%d",&x);
Insert(x);
Print();
}
}
2.任意位置插入或删除一个节点
(1)任意位置插入一个节点
插入函数:
void Insert(int data , int n)
{
Node* temp1 = new Node();
temp1->data = data;
temp1->next = NULL;
if(n == 1)
{
temp1->next = head;
head = temp1;
return;
}
Node* temp2 = head;
for(int i=0;i<n-2:i++)
{
temp2 = temp2->next;
}
temp1->next = temp2->next;
temp2->next = temp1;
}
代码解释:
void Insert(int data , int n) 中 data 作为值的传递,n 作为链表中位置的传递;
首先,new函数是C++的分配堆的空间,与笔记1.没有区别;
其次,我们对temp1->data进行赋值,temp1->next指向NULL;
第三,如果我们希望它作为第一个节点,那么我们让 temp1->next 指向头节点所指向的,头节点 head 指向temp1;
如果不是作为第一个节点,而是插入其他位置,那么我们需要引入另一个临时变量 temp2 代替头函数,因为我们不能改变头函数,后续我们还需要头函数重新进入该链表;
接下来,我们需要让这个链表从 temp2 开始循环,循环((n-2)-0)+1次,即n-1次;
现在,程序已经到达第n-1个节点,我们需要将 temp1 插入到第n个位置,只需要断开原来第n-1个节点至第n个节点的位置,然后将第n-1个节点连接 temp1 ,将 temp1 连接原来的第n个节点,现在我们的新链表诞生了, temp1 也在第n个节点上了。
输出函数
void Print()
{
struct Node* temp = head;
while(temp != NULL)
{
printf("%d ",temp->data);
temp = temp->next;
}
printf("\n");
}
与前文的输出函数一样。
主函数:
int main()
{
head = NULL;
Insert (x,y);
Print();
}
代码解释:
这里的x,y 是准确的值,不是变量;
下文如若没有说明,可自行理解。
(2)任意位置删除一个节点
接下来,我们进行任意位置节点的删除,且Print函数和Insert函数不在赘述。
删除函数:
void Delete(int n)
{
struct Node* temp1 = head;
if(n == 1)
{
head = temp1->next ;
free(temp1);
return;
}
int i;
for(int i=0;i<n-2;i++)
temp1 = temp1->next;
struct Node* temp2 = temp1->next;
temp1->next = temp2->next;
free(temp2);
}
删除函数与插入函数的思想很相似:
我们对temp1代替头节点;
如果我们希望删除第一个节点,那么我们让 temp1->next 指向头节点所指向的,此时释放 temp1 所占用的堆空间就ok了;
如果不是作为第一个节点,而是删除指定位置,我们需要让这个链表从 temp1 开始循环,依旧循环n-1次,到达第n-1个节点,此时引入另一个临时变量 temp2 指向 temp1->next 所指向的节点,此时 temp2 代替了temp1中第n个节点
我们需要将第n-1个节点的 temp1 ->next 指向第n+1个节点,所以需要让第n个节点的temp1->next赋值给第n-1个节点的temp1->next;
最后释放temp2所占用的堆空间,现在我们的新链表诞生了,第n个节点也删除了。
主函数:
int main()
{
head =NULL;
Insert(x,y);
int n;
scanf("%d",&n);
Delete(n);
Print();
}
代码解释:
其中 n 为需要删除的节点位置。
3.反转链表之迭代
结构体:
struct Node
{
int data;
struct Node* next;
};
struct Node* head;
void Reverse()
{
sruct Node* crruent ,* prev,* next;
current = head;
prev = NULL;
while(current != NULL)
{
next = current->next;
current->next = prev;
prev = current;
current = next;
}
head =prev;
}
current现在为链表;
我们可以把该函数看作一个工具,对链表里面的所有节点进行改造;
我们需要反向目前节点与上一个的关系,让第n个节点指向第n-1个节点(这里对应while循环里面的第1,2行);
接下来,我们需要改造第n+1个节点,所以我们需要把 current 传给 prev,把 next 传给 current(这里对应while循环里面的第3,4行)。
这里我目前不在写主函数和其他函数。
4.递归打印链表
打印函数与前面的区别:
在这里,我不再使用while循环进行打印,我用递归的方法进行打印,这样我可以在不使用反转函数的前提下,依然打印出反转的链表。
代码:
Print函数进行正向打印:
void Print(struct Node* p)
{
if (p == NULL)
return;
printf("%d ",p->data);
Print(p->next);
}
RPrint函数进行反向打印:
void RPrint(struct Node* p)
{
if (p == NULL)
return;
RPrint(p->next);
printf("%d ",p->data);
}
代码解释:
正向霍纳好理解,我们直接进入RPrint函数:
首先判断该指针是否为空指针,是则退出,否则进行递归直到最后一个节点;
现在,程序已经到最后一个节点了,假设有n个节点,我们对其进行打印,然后返回第n-1个节点,打印,再返回第n-2个节点,打印,如此循环直至打印完第一个节点。
(如果有不理解的,那一定是递归没有掌握,我建议重新学习递归再看RPrint函数,相信你会大有长进的)
5.反转链表之递归
使用递归进行反转链表,与迭代的区别:
(1)迭代:复反馈过程的活动,每一次迭代的结果会作为下一次迭代的初始值。
(2)递归:一直调用自己。
(3)代码:
结构体:
struct Node
{
int data;
struct Node* next;
};
struct Node* head;
反转函数
void Reverse(strcut Node* p)
{
if (p->next == NULL)
{
head = p;
return;
}
Reverse(p->next);
struct Node* q = p->next;
q->next = p;
p->next = NULL;
}
代码解释-void Reverse:
如果 p->next 不为NULL,则跳过 if 语句,进入 Reverse 函数,进行递归,直至 p->next 为NULL,结束递归,执行 struct Node* q =p->next 这一语句。
我们现在假设链表有5个节点
现在进行第一次跳出,头指针的值为p[4],q[0]为p[4],q[1]为p[3],p[4]为NULL;
进行第二次跳出,q[1]为p[3],q[2]为p[2],p[3]为NULL;
直至q[4]为p[0],那么现在有q链表,而p链表的所有值已经是NULL。
6.双向链表
(1)定义:它的每个数据结点中都有两个指针 ,分别指向直接后继和直接前驱。
(2)特点:它需要3个部分,第一个部分指向前一个数据的地址,第二个部分为数据本身,第三个部分指向后一个数据的地址。
(3)优点:可以反向查询。
(4)缺点:查询慢,如果要访问链表中一个元素,就需要从第一个元素遍历查找;需要额外的内存
(5)代码:分为两个部分,插入,删除。
构建一个结构体:
struct Node
{
int data;
struct Node* next;
struct Node* prev;
};
struct Node* head;
创建一个新节点:
struct Node* GetNewNode(int x);
{
struct Node* newNode = (struct Node*) malloc (sizeof (struct Node));
newNode->data = x;
newNode->prev = NULL;
newNode->next = NULL;
return newNode;
}
头部插入:
void InsertAtHead(int x)
{
struct Node* newNode = GetNewNode(x);
if (head == NULL)
{
head = newNode;
return;
}
head->prev = newNode;
newNode->next = head;
head = newNode;
}
代码解释-void InsertAtHead:
我们先将 函数GetNewNode 传进 InsertAtHead 中,先进行判断,如果链表中只有头节点,那么链表对头节点下一个传出的就是newNode,我们称之为newNode1,接下来退出这个函数。
如果链表已经有一个及以上的节点了,那么我们 head->prev 即就是 newNode1->prev 将指向 newNode2 ,而 newNode2 通过指向头节点,头节点指向 newNode1 来指向newNode1 ,我们此时再将头节点指向 newNode2 ,这样就插入一个头节点。
正向打印:
void Print()
{
struct Node* temp = head;
printf("Forward:");
while(temp != NULL)
{
printf("%d ",temp->data);
temp = temp->next;
}
printf("\n");
}
反向打印:
void RevesrePrint()
{
struct Node* temp = head;
if(temp == NULL)
return;
while(temp->next!=NULL)
{
temp = temp->next;
}
printf("Reverse:");
while(temp != NULL)
{
printf("%d ",temp->data);
temp = temp->prev;
}
printf("\n");
}
正向打印不在解释,我们对反向打印进行学习:
首先建立一个头指针,用 temp 命名;
接下来进入if判断,避免空指针的出现;
其次进入while循环,目的是到达最后一个节点;
最后,利用temp->prev进行打印。
主函数:
main()
{
head = NULL;
InsertAtHead(x);
Print();
ReversePrint();
}
接下来我们进行尾部节点的插入:
前提:
我们需要构建一个全局变量 temp ,主要用来在尾部节点相连时进行中间量。
void InsertAtTail(int x)
{
struct Node* newNode = GetNewNode(x);
if (head == NULL)
{
head = newNode;
temp = head;
return;
}
temp->next = newNode;
newNode->prev = temp;
temp = newNode;
}
代码的思路与头部插入一样。
最后以上内容就是这么多,后续我可能会再b站发一起视频配合这个进行讲解。
多加练习,会很容易了解数据结构的。
Harsha Suryanarayana的熟肉
同时也感谢up主fengmuzi2003的翻译。