逻辑结构上一个挨一个的数据,在实际存储时,并没有像顺序表那样也相互紧挨着。恰恰相反,数据随机分布在内存中的各个位置,这种存储结构称为线性表的链式存储。

由于分散存储,为了能够体现出数据元素之间的逻辑关系,每个数据元素在存储的同时,要配备一个指针,用于指向它的直接后继元素,即每一个数据元素都指向下一个数据元素(最后一个指向NULL(空))。


图1 链式存储存放数据

如图1所示,当每一个数据元素都和它下一个数据元素用指针链接在一起时,就形成了一个链,这个链子的头就位于第一个数据元素,这样的存储方式就是链式存储。

线性表的链式存储结构生成的表,称作“链表”。

链表中数据元素的构成

每个元素本身由两部分组成:

  1. 本身的信息,称为“数据域”;
  2. 指向直接后继的指针,称为“指针域”。
 

图2 结点的构成

这两部分信息组成数据元素的存储结构,称之为“结点”。n个结点通过指针域相互链接,组成一个链表。


图3 含有n个结点的链表
 

图 3 中,由于每个结点中只包含一个指针域,生成的链表又被称为 线性链表 或 单链表。

链表中存放的不是基本数据类型,需要用结构体实现自定义:

  1. typedef struct Link
    {
  2.   char elem;  //代表数据域
  3.   struct Link * next;  //代表指针域,指向直接后继元素
  4. }link;

头结点、头指针和首元结点

头结点:有时,在链表的第一个结点之前会额外增设一个结点,结点的数据域一般不存放数据(有些情况下也可以存放链表的长度等信息),此结点被称为头结点。

若头结点的指针域为空(NULL),表明链表是空表。头结点对于链表来说,不是必须的,在处理某些问题时,给链表添加头结点会使问题变得简单。

首元结点:链表中第一个元素所在的结点,它是头结点后边的第一个结点。

头指针:永远指向链表中第一个结点的位置(如果链表有头结点,头指针指向头结点;否则,头指针指向首元结点)。

头结点和头指针的区别:头指针是一个指针,头指针指向链表的头结点或者首元结点;头结点是一个实际存在的结点,它包含有数据域和指针域。两者在程序中的直接体现就是:头指针只声明而没有分配存储空间,头结点进行了声明并分配了一个结点的实际物理内存。
图 4 头结点、头指针和首元结点

单链表中可以没有头结点,但是不能没有头指针!

链表的创建和遍历

万事开头难,初始化链表首先要做的就是创建链表的头结点或者首元结点。创建的同时,要保证有一个指针永远指向的是链表的表头,这样做不至于丢失链表。

例如创建一个链表(1,2,3,4):

  1. link * initLink()
    {
  2.   link * p = (link*)malloc(sizeof(link));  //创建一个头结点
  3.   link * temp = p;  //声明一个指针指向头结点,用于遍历链表
  4.   //生成链表
  5.   for (int i=; i<; i++)
      {
  6.     link *a = (link*)malloc(sizeof(link));
  7.     a->elem = i;
  8.     a->next = NULL;
  9.     temp->next = a;
  10.     temp = temp->next;
  11.   }
  12.   return p;
  13. }

链表中查找某结点

一般情况下,链表只能通过头结点或者头指针进行访问,所以实现查找某结点最常用的方法就是对链表中的结点进行逐个遍历。

实现代码:

  1. int selectElem(link * p, int elem)
    {
  2.   link *t = p;
  3.   int i = ;
  4.   while (t->next)
      {
  5.     t = t->next;
  6.     if (t->elem == elem)
        {
  7.       return i;
  8.     }
  9.     i++;
  10.   }
  11.   return -;
  12. }

链表中更改某结点的数据域

链表中修改结点的数据域,通过遍历的方法找到该结点,然后直接更改数据域的值。

实现代码:

  1. //更新函数,其中,add 表示更改结点在链表中的位置,newElem 为新的数据域的值
  2. link *amendElem(link * p, int add, int newElem)
    {
  3.   link * temp = p;
  4.   temp = temp->next;  //在遍历之前,temp指向首元结点
  5.   //遍历到被删除结点
  6.   for (int i=; i<add; i++)
      {
  7.     temp = temp->next;
  8.   }
  9.   temp->elem = newElem;
  10.   return p;
  11. }

向链表中插入结点

链表中插入头结点,根据插入位置的不同,分为3种:
  1. 插入到链表的首部,也就是头结点和首元结点中间;
  2. 插入到链表中间的某个位置;
  3. 插入到链表最末端;

图 5 链表中插入结点5

虽然插入位置有区别,都使用相同的插入手法。分为两步,如图 5 所示:

  • 将新结点的next指针指向插入位置后的结点;
  • 将插入位置前的结点的next指针指向插入结点;

提示:在做插入操作时,首先要找到插入位置的上一个结点,图4中,也就是找到结点 1,相应的结点 2 可通过结点 1 的 next 指针表示,这样,先进行步骤 1,后进行步骤 2,实现过程中不需要添加其他辅助指针。

实现代码:

  1. link * insertElem(link * p, int elem, int add)
    {
  2.   link * temp = p;  //创建临时结点temp
  3.   //首先找到要插入位置的上一个结点
  4.   for (int i=; i<add; i++)
      {
  5.     if (temp == NULL)
       {
  6.       printf("插入位置无效\n");
  7.       return p;
  8.     }
  9.     temp = temp->next;
  10.   }
  11.   //创建插入结点c
  12.   link *c = (link*)malloc(sizeof(link));
  13.   c->elem = elem;
  14.   //向链表中插入结点
  15.   c->next = temp->next;
  16.   temp->next = c;
  17.   return p;
  18. }

注意:首先要保证插入位置的可行性,例如图 5 中,原本只有 5 个结点,插入位置可选择的范围为:1-6,如果超过6,本身不具备任何意义,程序提示插入位置无效。

从链表中删除节点

当需要从链表中删除某个结点时,需要进行两步操作:

  • 将结点从链表中摘下来;
  • 手动释放掉结点,回收被结点占用的内存空间;
使用malloc函数申请的空间,一定要注意手动free掉。否则在程序运行的整个过程中,申请的内存空间不会自己释放(只有当整个程序运行完了以后,这块内存才会被回收),造成内存泄漏,别把它当成是小问题。

实现代码:

  1. link * delElem(link * p,int add)
    {
  2.   link * temp = p;
  3.   //temp指向被删除结点的上一个结点
  4.   for (int i=; i<add; i++)
      {
  5.     temp = temp->next;
  6.   }
  7.   link * del = temp->next;//单独设置一个指针指向被删除结点,以防丢失
  8.   temp->next = temp->next->next;//删除某个结点的方法就是更改前一个结点的指针域
  9.   free(del);//手动释放该结点,防止内存泄漏
  10.   return p;
  11. }

完整代码

  1.  

#include <stdio.h>

#include <stdlib.h>

typedef struct Link
{
  

  int elem;
  

  struct Link *next;

}link;

link * initLink();
//链表插入的函数,p是链表,elem是插入的结点的数据域,add是插入的位置

link * insertElem(link * p,int elem,int add);
//删除结点的函数,p代表操作链表,add代表删除节点的位置

link * delElem(link * p,int add);
//查找结点的函数,elem为目标结点的数据域的值

int selectElem(link * p,int elem);
//更新结点的函数,newElem为新的数据域的值

link *amendElem(link * p, int add, int newElem);

void display(link *p);

int main()
{
  

  //初始化链表(1,2,3,4)
  

  printf("初始化链表为:\n");
  

  link *p = initLink();
  

  display(p);
  

  printf("在第4的位置插入元素5:\n");
  

  p = insertElem(p, , );
  

  display(p);
  

  printf("删除元素3:\n");
  

  p = delElem(p, );
  

  display(p);
  

  printf("查找元素2的位置为:\n");
  

  int address = selectElem(p, );
  

  if (address == -)

    printf("没有该元素");
    

  else

    printf("元素2的位置为:%d\n",address);
    

  printf("更改第3的位置的数据为7:\n");
  

  p = amendElem(p, , );
  

  display(p);
  

  return ;

}

link * initLink()
{
  

  link * p = (link*)malloc(sizeof(link));//创建一个头结点
  

  link * temp = p;//声明一个指针指向头结点,用于遍历链表
  

  //生成链表
  

  for (int i=; i<; i++)
  {
    

    link *a = (link*)malloc(sizeof(link));
    

    a->elem=i;
    

    a->next=NULL;
    

    temp->next=a;
    

    temp=temp->next;
  

  }
  

  return p;

}

link *insertElem(link * p, int elem, int add)
{
  

  link * temp = p;  //创建临时结点temp
  

  //首先找到要插入位置的上一个结点
  

  for (int i=; i<add; i++)
  {
    

    if (temp == NULL)
    {
      

      printf("插入位置无效\n");
      

      return p;
    

    }
    

    temp = temp->next;
  

  }
  

  //创建插入结点c
  

  link * c = (link*)malloc(sizeof(link));
  

  c->elem = elem;
  //向链表中插入结点
  

  c->next = temp->next;
  

  temp->next = c;
  

  return p;

}

link * delElem(link * p, int add)
{
  

  link * temp = p;
  //遍历到被删除结点的上一个结点
  

  for (int i=; i<add; i++)
  {
    

    temp = temp->next;
  

  }
  

  link * del = temp->next;    //单独设置一个指针指向被删除结点,以防丢失
  

  temp->next = temp->next->next;    //删除某个结点的方法就是更改前一个结点的指针域
  

  free(del);    //手动释放该结点,防止内存泄漏
  

  return p;

}

int selectElem(link * p, int elem)
{
  

  link *t = p;
  

  int i = ;
  

  while (t->next)
  {
    

    t = t->next;
    

    if (t->elem == elem)  

      return i;
       

    i++;
  

  }
  

  return -;

}

link *amendElem(link * p, int add, int newElem)
{
  

  link * temp = p;
  

  temp = temp->next;  //tamp指向首元结点
  

  //temp指向被删除结点
  

  for (int i=; i<add; i++)
  {
    

    temp = temp->next;
  

  }
  

  temp->elem = newElem;
  

  return p;

}

void display(link *p)
{
  

  link* temp = p;//将temp指针重新指向头结点
  

  //只要temp指针指向的结点的next不是Null,就执行输出语句。
  

  while (temp->next)
  {
    

    temp = temp->next;
    

    printf("%d", temp->elem);
  

  }
  

  printf("\n");

}

运行结果:

  1. 初始化链表为:
  2. 1234
  3. 在第4的位置插入元素5
  4. 12354
  5. 删除元素3:
  6. 1254
  7. 查找元素2的位置为:
  8. 元素2的位置为:2
  9. 更改第3的位置的数据为7:
  10. 1274

总结

线性表的链式存储相比于顺序存储,有两大优势:

  1. 链式存储的数据元素在物理结构没有限制,当内存空间中没有足够大的连续的内存空间供顺序表使用时,可能使用链表能解决问题。(链表每次申请的都是单个数据元素的存储空间,可以利用上一些内存碎片)
  2. 链表中结点之间采用指针进行链接,当对链表中的数据元素实行插入或者删除操作时,只需要改变指针的指向,无需像顺序表那样移动插入或删除位置的后续元素,简单快捷。

链表和顺序表相比,不足之处在于,当做遍历操作时,由于链表中结点的物理位置不相邻,使得计算机查找起来相比较顺序表,速度要慢。

数据结构5: 链表(单链表)的基本操作及C语言实现的更多相关文章

  1. 数据结构——Java实现单链表

    一.分析 单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素.链表中的数据是以结点来表示的,每个结点由元素和指针构成.在Java中,我们可以将单链表定义成一个类,单链表的基 ...

  2. js数据结构与算法--单链表的实现与应用思考

    链表是动态的数据结构,它的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成. 现实中,有一些链表的例子. 第一个就是寻宝的游戏.你有一条线索,这条线索是指向寻找下一条线 ...

  3. PHP数据结构之实现单链表

    学习PHP中,学习完语法,开始尝试实现数据结构,今天实现单链表 <?php class node //节点的数据结构 { public $id; public $name; public $ne ...

  4. Java数据结构——链表-单链表

    <1>链表 <2>引用和基本类型 <3>单链表 //================================================= // Fil ...

  5. C++ 数据结构学习二(单链表)

    模板类 //LinkList.h 单链表#ifndef LINK_LIST_HXX#define LINK_LIST_HXX#include <iostream>using namespa ...

  6. C#数据结构与算法系列(四):链表——单链表(Single-LinkedList)

    1.介绍: 链表是有序的列表,但是它在内存的存储如下:  链表是以节点的方式来存储,链式存储 每一个节点包含data域,next域:指向下一个节点 链表的各个节点不一定是连续存储 链表分带头节点的链表 ...

  7. 数据结构:DHUOJ 单链表ADT模板应用算法设计:长整数加法运算(使用单链表存储计算结果)

    单链表ADT模板应用算法设计:长整数加法运算(使用单链表存储计算结果) 时间限制: 1S类别: DS:线性表->线性表应用 题目描述: 输入范例: -5345646757684654765867 ...

  8. 线性表->链式存储->线形链表(单链表)

    文字描述: 为了表示前后两个数据元素的逻辑关系,对于每个数据元素,除了存储其本身的信息之外(数据域),还需存储一个指示其直接后继的信息(即直接后继的存储位置,指针域). 示意图: 算法分析: 在单链表 ...

  9. pta 奇数值结点链表&&单链表结点删除

    本题要求实现两个函数,分别将读入的数据存储为单链表.将链表中奇数值的结点重新组成一个新的链表.链表结点定义如下: struct ListNode { int data; ListNode *next; ...

  10. 数据结构-多级指针单链表(C语言)

    偶尔看到大一时候写了一个多级链表,听起来好有趣,稍微整理一下. 稍微注意一下两点: 1.指针是一个地址,他自己也是有一个地址.一级指针(带一个*号)表示一级地址,他自身地址为二级地址.二级指针(带两个 ...

随机推荐

  1. 2015.4.25利用UIAutomation 替代API函数,解决了ListView无法读数据的难题,顺便实现了鼠标模拟滚轮

    UIAutomation比API的优点是类似于消息处理机制,而不是主要靠模拟鼠标键盘发送消息 首先添加引用UIAutomationClient和UIAutomationTypes,在安装.net3.5 ...

  2. redis学习二 排序

    文章转载自:http://www.cnblogs.com/redcreen/archive/2011/02/15/1955226.html redis支持对list,set和sorted set元素的 ...

  3. DAY10-MYSQL存储引擎

    一 什么是存储引擎 mysql中建立的库===>文件夹 库中建立的表===>文件 现实生活中我们用来存储数据的文件有不同的类型,每种文件类型对应各自不同的处理机制:比如处理文本用txt类型 ...

  4. 使用php输出时间格式

    <? date_default_timezone_set("ETC/GMT-8"); $tm=time(); echo date("Y-m-d h:i a" ...

  5. Python03 字符串类型、强制类型转化、列表、元组、字典、集合

    1 字符串类型 在python中字符串类型用str表示,字符串的连接用 + 1.1 创建字符串对象 ·创建一个字符串对象有两种方式,一种方式是直接用字符串进行赋值,另外一种是利用str类实例化对象:具 ...

  6. 生产者与消费者-N:N-基于list

    多个生产者/多个消费者: /** * 生产者 */ public class P { private MyStack stack; public P(MyStack stack) { this.sta ...

  7. mac 彻底卸载Paragon NTFS

    之前安装了paragon NTFS,试用期过了就卸载了,但是每天还是会提示“试用期已到期”,看着很烦. 百度了一下,发现网上的版本可能比较老了,和我的情况不太一样,但道理应该是一样的. 彻底删除方法: ...

  8. Spring第四篇

    在spring第三篇中介绍了bean元素属性 在第四篇中介绍spring注入的方式 1 set方法注入 建立一个User类 创建私有的属性 set  get 方法  重写toString方法 代码如下 ...

  9. UNIX和Linux信号

    1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号).不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失 ...

  10. java的import关键字的使用

    在java中如何使用Java包中自带的类呢? 方法一: 在使用时可以用Java.(包名).(方法名).(包中的类名): 例如:Java.util.Arrays.toString(某个要排序数组); 具 ...