链表
本文仅对单向链表进行讲解,其余链表类型不做过多讲解。
一、定义:链表是线性表的链接存储表示
二、分类:单向链表、静态链表、双向链表、循环链表
三、特点:每个元素由数据域和指针域构成
1.结点可以连续,也可以不连续存储;
2.结点的逻辑顺序与物理顺序可以不一致;
3.表的容量可以不断变大(顺序表是固定的)。
四、插入结点
head为头结点,a为待插入的结点,假设a的地址为0x100;
对于结点的插入,说白了,就是交换两个地址;
a ->next = head ->next;
head ->next = a;
第一步、将head->next,也就是NULL赋给a->next,这样a指向的下一个就是NULL
第二步、将a的起始地址赋给head->next,也就是让head指向a,通过这两步就可以将a插入到head的后面。
对于下面这种情况该如何将p插入到head后面呢?
其实,这和上面是一样的操作,只需要操作两个地址;
p ->next = head ->next;
head ->next = p;
第一步、将p指向的下一个地址与head指向的下一个地址一致,这样就实现p指向原来head指向的下一个元素;
第二步、将p的地址赋给head指向的下一个地址,就是把p接到了head的后面。
五、删除结点
其实要删除中间这个元素,我们只需要将这个元素之后的元素的地址赋给要删除的元素前面一个元素指向的下一个地址就好(听起来就晕了!!!),看上面那个带有弧度的箭头,我想大家就明白我说的意思了;
假设这个链表的类型叫做LinkList,我们首先定义一个q指针,指向被删除的元素:LinkList *q = p ->next;
这时候,我们将q后面元素的地址(也就是q->next)赋给前面一个元素指向的下一个地址(也就是p->next)
p->next = q->next;
最后我们只要把q给释放掉就万事大吉啦:free(q);
整体语句如下:
LinkList *q = p ->next;
p->next = q->next;
free(q);
Tips:刚刚接触链表的时候,对链表的数据域和指针域不是很明白,经常会纠结于p->next 的地址为什么是q的地址,对于一个结点元素来说,数据域指向这个地址所对应的数据,而指针域就是指向下一个结点的地址,所有会出现我图中一头一尾地址相同的情况,原谅我这么粗糙的解释吧!
// 单向链表
#include <stdio.h>
#include <stdlib.h>
typedef struct node
{
int data;
struct node *next;
}node;
// 创建链表
node *create()
{
int i = 0;
node *head, *p, *q = NULL;
int x = 0;
head = (node *)malloc(sizeof(node));
head->next = NULL;
while (1)
{
printf("Please input the data:\n");
scanf("%d", &x);
if (x < 0)
break;
p = (node *)malloc(sizeof(node));
p->next = NULL;
p->data = x;
if (++i == 1)
{
head->next = p;
}
else
{
q->next = p;
}
q = p;
}
return head;
}
// 测量链表长度
int LengthOfLink(node *head)
{
int len = 0;
node *p;
p = head->next;
while (p != NULL)
{
len++;
p = p->next;
}
return len;
}
// 查找链表中的某一节点
node *search_node(node *head, int pos)
{
node *p = head->next;
if (pos < 0)
{
printf("Incorrect position to search node!\n");
return NULL;
}
if (pos == 0)
{
return head;
}
if (p == NULL)
{
printf("Link i empty!\n");
return NULL;
}
while (--pos)
{
if ((p = p->next) == NULL)
{
printf("Incorrect position to search node\n");
break;
}
}
return p;
}
// 插入结点,后插法,在指定位置的结点之后插入
node *Insert(node *head, int pos, int data)
{
node *item = (node *)malloc(sizeof(node));
node *p;
item->data = data;
if (pos == 0)
{
item->next = head->next;
head->next = item;
return head;
}
p = search_node(head, pos);
if (p != NULL)
{
item->next = p->next;
p->next = item;
}
return head;
}
// 结点删除
node *Delete(node *head, int pos)
{
node *item = NULL;
node *p = head->next;
if (p == NULL)
{
printf("Link is empty\n");
return NULL;
}
p = search_node(head, pos - 1);
if (p != NULL && p->next != NULL)
{
item = p->next;
p->next = item->next;
free(item);
}
return head;
}
// 链表的遍历
void print(node *head)
{
node *p = head->next;
if (p != NULL)
{
while (p)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
}
int main()
{
node *head = create();
Insert(head, 0, 1);
printf("The length of link is %d\n", LengthOfLink(head));
printf("Before delete\n");
print(head);
Delete(head, 1);
printf("After delete\n");
print(head);
return 0;
}
那么顺序表和链表的区别在哪呢?
存储分配的方式:
顺序表的存储空间是静态分配的
链表的存储空间是动态分配的
顺序表的存储空间是静态分配的
链表的存储空间是动态分配的
存储密度 = 结点数据(data)本身所占的存储量/结点结构所占的存储总量
顺序表的存储密度 = 1
链表的存储密度 < 1
插入/删除时移动元素个数:
顺序表平均需要移动近一半元素
链表不需要移动元素,只需要修改指针
若插入/删除仅发生在表的两端,宜采用带尾指针的循环链表
Tips:链表的逆序(很重要!很重要!很重要!)
顺序表的存储密度 = 1
链表的存储密度 < 1
插入/删除时移动元素个数:
顺序表平均需要移动近一半元素
链表不需要移动元素,只需要修改指针
若插入/删除仅发生在表的两端,宜采用带尾指针的循环链表
Tips:链表的逆序(很重要!很重要!很重要!)
下面我们将代码结合图片来讲解如何解决这个问题:
首先附上代码
struct ListNode
{
int data;
struct ListNode *next;
};
int ReverseLinkList( LinkList A )
{
if (A==NULL) return 0;
ListNode *p = A->next;
A->next = NULL;
while (p!=NULL)
{
ListNode *q = p;
p = p->next;
q->next = A->next;
A->next = q;
}
return 1;
}
在讲解之前,说明一点,表头并不参与逆序,表头一直都是作为表头而存在的,逆序只是把表头后面的结点交换位置
为了更好的理解,我人为的为这些结点分配了地址。
第一步:我们定义一个名为A的链表,当链表为空时,直接return 0退出;当链表不为空时,定义一个p指针指向A->next,开始时,A->next指向0x100的位置,随后将A->next指向NULL,并又定义一个q指针指向p,就如上图那样,此时p和q指向同一地址。之后将p又指向了p->next的地址,就是下图的0x300。将A->next指向的NULL赋给了q->next,也就是说q->next指向的是NULL。再将q的地址赋给A->next,为什么我们要这么做呢?别急,待会你会明白的。
第二步:第二次循环执行到p = p->next时,链表情况如下图;此时p已经指向最后一个元素,而q也指向p原来的位置,换句话说,q也向后挪了一位。
当执行q->next = A->next; A->next = q;这两句话的意义在于,将q指向的下一个地址变成了0x100,将A->next指向的地址变为0x300;此时,不难发现,1已经接到了2的后面,那么我们为什么要反反复复对A->next指向的地址进行修改呢?我想大家应该有些眉目了。第二次循环结束,链表得情况如下图;
第三步:第三次循环,依旧是将p和q向后挪了一位,此时p指向NULL,q指向第三个结点的地址(即0x800)。随后执行执行q->next = A->next; A->next = q;将0x300的地址赋给了p->next,0x800的地址赋给了A->next;这步操作如下图;
最终的结果如下,根据物理地址的连接顺序,我们将这个链表逆序;
那么A->next到底在这个程序当中扮演着什么样的角色呢?
答案是:媒介
通过不断的对A->next进行操作,实际上是通过它将其后的地址进行嫁接,首先将1的指针域指向NULL,也就是最后结点,随后将1连接到2的后面,再将2连接到3的后面,最后将3连接到A的后面,这样就完成了逆序的操作了。