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

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


图1 链式存储存放数据

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

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

链表中数据元素的构成

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

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

图2 结点的构成

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


图3 含有n个结点的链表
 

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

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

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

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

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

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

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

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

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

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

链表的创建和遍历

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

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

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;
}

链表中查找某结点

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

实现代码:

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 -;
}

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

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

实现代码:

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

向链表中插入结点

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

图 5 链表中插入结点5

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

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

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

实现代码:

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;
}

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

从链表中删除节点

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

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

实现代码:

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

完整代码


#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");

}

运行结果:

初始化链表为:
1234
在第4的位置插入元素5:
12354
删除元素3:
1254
查找元素2的位置为:
元素2的位置为:2
更改第3的位置的数据为7:
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. BA 新web化 问题汇总

    1. 3D堆栈图在winform端无法显示,但在web端可以正常显示,说明与浏览器版本有关,在 IE 中设置文档模式为 IE8 即报错,IE9 却正常显示,可在 <head>节点下添加如下 ...

  2. 12-01Js对象##正则表达式##

    正则表达式:是一个字符一个字符的验证,通过量词验证字符串: 1.什么是RegExp?RegExp是正则表达式的缩写.RegExp 对象用于规定在文本中检索的内容. 2.定义RegExp:var +变量 ...

  3. 部署和调优 2.4 tomcat安装

    下载tamcet 官网 http://tomcat.apache.org/ 左侧选择版本 复制下载链接 切换到下载目录 cd /usr/local/src linux wget wget http:/ ...

  4. JavaScript的运算符

    一.什么是表达式??? 是ECMScript中的一个短语,解释器可以通过计算把它转成一个值,最简单的表达式是字面量或者变量名,单一字面量和组合字面量统称为表达式. 二.一元运算符 1.delete 运 ...

  5. valgrind详解

    调不尽的内存泄漏,用不完的Valgrind Valgrind 安装 1.valgrind 安装包下载地址:http://valgrind.org/downloads/repository.html(使 ...

  6. Blender 基础 骨架-02 骨架的各种呈现方式

    Blender 基础 骨架-02 - 骨架的各种呈现方式 我使用的Blender版本:Blender V 2.77 前言 在 Blender 基础 骨架-01 教程里面,将骨架和模型联系在一起,我们在 ...

  7. vray学习笔记(1)vray介绍

    vray是个什么东西? 它是个渲染器. 渲染器是个什么东西? 渲染器就是3d软件里面把模型画成一张图片的东西,渲染的过程就是把3D物体变成2D画面的过程. 模型是个什么东西? 模型就是模型,它由两部分 ...

  8. 解决校园Dr客户端端口占用问题(2)

    win + R -> 输入cmd回车 -> 输入netsh winsock reset重启 -> 好了享受上网的快乐吧骚年

  9. 20.LIBRARY_PATH和LD_LIBRARY_PATH环境变量的区别

    转载:https://www.cnblogs.com/panfeng412/archive/2011/10/20/library_path-and-ld_library_path.html LIBRA ...

  10. HDU3686 Traffic Real Time Query

    按照vdcc缩点之后一条边只会属于一个新的点集,由于这棵树上满足(不是割点) - (割点) - (不是割点)的连接方法,所以求两条边之间的必经点就是(树上距离 / 2),倍增跳lca即可 考虑到缩点后 ...