最近实现了单链表的创建,插入,删除,寻找元素,以及输出,删除链表的操作。这里记录下自己对链表的理解。
首先是链表,链表有两个属性,一个是数据属性(域),另一个是指针属性(域),链表的理解通俗的来讲可以想成老师,让学生手拉手连接起来,这样就形成了一条人形长链,每一个学生,就是一个 “结点”,老师是头结点(head)。要一个结点就找一个学生拉上去;不要一个结点就让那个学生手放开,空出的两个学生手拉在一起……
我也可以把链表想象成一条铁链,要一个结点就焊接一个上去;不要一个就敲碎一个,把敲碎那个的旁边两个重新焊接起来;要加一个数据上去就把要插入的位置的那个节点给融开,把新的结点焊上去。
- 与链表有关的两个函数:
1:动态申请内存 malloc 函数 (memory allocation)
其原型是:
extern void *malloc(unsigned int num_bytes);
void *
是未确定类型的指针,要用户指定类型,如char *, int *,double *
若没有指定则会报错.
如果申请成功,则返回被指向的内存的指针,否则返回 NULL
与之相对应的是free()函数,用来释放由malloc给指针变量申请内存空间。
其原型是:
void free(void *P)
- 结点结构体的声明
struct node
{
int date; //数据域
struct node *next; //指针域
};
- 链表头结点的建立
代码实现:
void ListInitiate(node **head) //二级指针
{
if( (*head = (node *)malloc(sizeof(node))) == NULL)
exit(1);
(*head)->next = NULL;
}
这里传递的是主函数中指针变量head的地址,而不是值。
- 链表的数据录入
void ListDate(node **head) //二级指针
{
node *p1, *p2;
p1 = p2 = (node *)malloc(sizeof(node));
scanf("%d", &p1->date);
(*head)->next = p1; //重要: 头指针的指针域指向第一个结点
while(p1->date != 0) //结束条件
{
p1 = (node *)malloc(sizeof(node)); //新的节点
scanf("%d",&p1->date); //数据录入
p2->next = p1; //指向新的结点所在的位置
p2 = p1; //存放新开辟的内存,便于下一次使用
}
p2->next = NULL; //最后一个结点指向NULL
}
在主函数中,我将头指针的地址传过去,因为如果将头指针传的值过去,是无法达到建立数据的效果,所以要传递指针的地址,也就是在 ListDate() 函数的参数列表内要声明一个二级指针。
此函数的作用,先给p1, p2动态分配一个大小为 sizeof(node) 的内存空间。
头指针的指针域指向第一个结点所在的内存位置。
录入数据的结束条件为输入的数据为 0.
录入第二个数据的时候,要将第一个结点的指针域,指向第二个结点所在的内存位置。
即,录入第 n 个数据,将第 n - 1 个结点的指针域,指向第 n 个结点所在的内存位置。
这就是链表的连接处理,通过指针域,将一个个结点连接起来,达到整体的目的。这也就存在一个问题,不像数组那样,知道下标便可以直接输出数据,而是读取的时候都要从头指针遍历起。
注意 2 点:
- 最后一个结点的指针域,一定指向NULL。
- p -> next 中保存的,都是下一个结点所在的内存地址,这个内存地址是由 malloc 函数分配的,是一整块的,具有两个数据域的。(这点要着重理解)
- 数据的插入.
要求:在第 n 个结点前插入一个数据,成功返回1,失败返回0
代码实现:
int ListInsert(node *head, int n, int x)
{
int k = 0;
node *p1, *p2;
p1 = head;
while(k < n - 1 && p1 != NULL) //移动到n-1的位置并且p1不可以为空
{
p1 = p1->next;
}
if(k != n - 1) //若此时没有指到第 n - 1个结点
return 0;
if((p2 = (node *)malloc(sizeof(node))) == NULL) //新开辟一个内存
exit(0);
else
{
p2->date = x; //放入数据内容
p2->next = p1->next; //新的指针域指向第 n 个数据的位置
p1->next = p2; //当前第 n-1 个指向新开辟的内存位置
return 1;
}
}
假设要插入的为结点 x
要实现在第 n 个结点前插入一个数据,就必须得找到第 n - 1 个结点的地址,这样才能将数据插在第 n 个后面。
当指针到达第 n - 1 这个位置后,就将 x 的指针域指向第 n 个结点所在的内存地址。
即该句:p2->next = p1->next; 由于 p1 在前面指向了第 n - 1个地址,故 p1->next 是第n个所在的内存地址。
最后令第 n - 1 个节点指向新开辟的内存地址即可实现成功加入数据。
- 链表数据的删除
要求: 删除第 n个结点,成功返回1,x存放被删除的值
代码实现:
int ListDelete(node *head, int n, int *x)
{
node *p1, *fn; //fn为要释放的结点位置
int k = 0;
p1 = head;
while(k < n - 1 && p1->next != NULL && p1->next->next != NULL)
{
p1 = p1->next; //使指针指向第 n-1 个位置
*x = p1->next->date; //值的存放
k++;
}
if(k != n - 1)
return 0;
fn = p1->next; //存放第 n 个指针地址
p1->next = p1->next->next; //使第n-1个指针域指向第n+1个结点地址
free(fn); //删除完毕后要释放该内存
return 1;
}
删除第 n 个元素,就必须先移动到第 n - 1个位置,在将其前一个结点(n-1)和其后一个结点(n+1)连接起来,最后释放该结点内存即可。
注意:
p1->next = p1->next->next; 中, p1在前面的循环中,指到了第 n-1个结点,所以p1->next中存放的是下第 n个结点的地址,即p1->next->next的意思就是取出第 n个结点的指针域里面的值,而里面存放的是第 n+1个结点位置。
p1->next = p1->next->next; 也可以这样想:
P(n-1)->next = Node(n); //第n个结点地址是保存在前一个结点指针域中的
Node(n)->next = Node(n+1); //第n个结点的指针域中存放是第n+1个结点的位置
P(n-1)->next->next = Node(n+1); //故有...
我觉得还可以这样思考:
赋值号的左边是固定的,表示就是第n-1个结点的指针域,右边是动态的,一个next就表示向这链表前面移动一个,几个移动几次,这里从 n-1 移动了两次,故变成了第 n+1个结点位置。
感觉第二种思考方式更简单的…
啊,再次感觉指针理解起来难。
- 链表长度的计算
代码实现:
int ListLength(node *head)
{
int i = 0;
while(head != NULL) //当指针为空的时候,就表明到了链表尾巴了。
{
head = head->next;
i++;
}
return i;
}
- 链表数据的输出
代码实现:
void PrintList(node *head)
{
head = head->next; //头指针指向第一个结点
while(head != NULL) //为空的时候就跳出循环
{
printf("%d ", head->date);
head = head->next; //移动一个
}
}
- 链表的删除
代码实现:
void DestoryNode(node **head) //传递头指针变量的地址进来
{
node *p1, *f; //p1控制链表移动,f存放将要释放的结点
p1 = *head;
//int i=1;
while(p1 != NULL)
{
f = p1; //指针变量f为释放中介,移动一个释放一个
p1 = p1->next; //结点移动
free(f);
//观察链表的属性,可以得到一个物理连接的直观数据
//printf(fp,"\n第 %d 个结点,其地址为: %d, 其指针域为: %d",i++,p2,p2->next);
}
*head = NULL; //头指针指向空,防止野指针。
printf("释放成功\n");
}
这里注释掉的两段是我想看下链表的链接方式,可以更加直观的看到数据,也方便进一步理解链表是如何一个一个通过指针链接起来的。
到此,链表的一些基本操作已经完成(可能有错误的地方欢迎指出)。
记得当时在学习链表的时候,理解能力不够,便放弃了,经过了高中的洗礼,理解能力跟上了,再次看链表的时候,一下子就理解了这个抽象的东东,开心啊。这篇博文就简单的记录下我对于链表的理解吧哈哈哈。