其实本应该从一般性的表讲起的,先说顺序表,再说链表 。但顺序表的应用范围不是很广,而且说白了就是数组的高级版本,他的优势仅在于两点:1.逻辑直观,易于理解。2.查找某个元素只需要常数时间——O(1),而与此同时,因为每个单元的物理内存都是连续的,所以不便于移动,不便于精细化操作,每次插入和删除都会带来巨额的时间开销。什么叫“巨额时间开销”?  举个栗子:我要在开头加一个数进去,那我要把所有的元素都往后移一位,空出来一个位置,这就需要穿过整个表,假如这个表有1000万个元素,大家可以自己脑补一下要花多久,答案是O(n)。这是时间的浪费。而如果用来计算稀疏多项式,比如:x^100+x^1300+x,这会造成中间有很多存储单元里存的是0,没有任何意义,但却实实在在消耗了内存,这是空间浪费。

因为插入和删除的运行时间非常慢,而且表的大小还必须事先已知,所以一般不用简单数组来实现表这种结构。

现在我们需要一种灵活的方法来使我们突破连续储存带来的限制,那怎么办呢?就不连续储存呗,把空间离散化。每一个单元里面大体分为两部分,一边存数据,另一边存下一个单元的地址,这样一来,逻辑上仍然是连续的,而在物理内存中则是星罗棋布了。这就是我们要学的链表了。这是我们要学的第一种线性结构。

现在,我们要加入或者移除一个元素的时候,就不必担心会对全体数据造成影响了,只需要改动“链条”就好了,其他不变。这样就能减少增删的时间开销了。单链表的样子就像这样:

可以脑补一列火车车厢hhhhhh

当然火车车厢是双链表,我们现在简便起见,先介绍单链表。

删除的命令可以通过修改一个指针来实现,就像这样:

插入的话,我们需要申请一个新单元,怎么申请?printf("请给我一点内存,谢谢");

显然不是的,对吧。那该怎么做?

对!malloc函数,说到这个,我多说几句啊。首先,别拼错了,我之前会手滑打错,也遇到过记不住这个函数名字的小白,咱得记住它的意思“Memory Allocate“,内存分配,这个内存从哪分配的呢,总不会是操作系统凭空变出来的,它是从“堆(Heap)”上分配来的,这个堆也是我们以后要学的一种数据结构。

接着说插入,申请一个新单元之后,我们再做两次指针调整就好了,就像这样:

重点:整个链表的核心在于指针的调整,而首先,我们要“拉住”整个链表,也就是说我们需要一个引子,来牵住整个一长串的表,这个引子就相当于火车头。因为每一个单元的物理位置都是随机的,想找到下一个只能依靠前一个单元的尾针(毕竟这是单链表)。

操作指针时一定要小心,包括调整顺序和指向。否则就会像这样

我们的目标是成为老司机,不要翻车。

好了,大概的思路我们已经捋顺了,现在来说具体怎么做。

前面提到了,我们需要一个引子,具体做法是留出一个标志节点,习惯称为表头(header)。说是节点,其实仅仅是一个指针,没有存放数据的位置。像这样

这里注意一点:表头后的一个单元,叫做头节点,这个节点有数据域,但是也不存有效数据,它仅仅是证明表的存在性。(我初学的时候因为这点没搞透彻,导致代码运行时各种bug,大家要引以为戒啊……)

接下来是代码实现,首先约定一些名字

 struct Node;                //先声明一个节点,后面定义
 typedef struct Node *PtrToNode;     /*声明节点指针,并且将其类型替换为PtrToNode(替换之前是struct Node*),这样做的目的是方便我们理解*/
  
  
 typedef PtrToNode List; //将struct Node*再次替换为List(表),进一步直观化
 typedef PtrToNode Position;//将struct Node*再次替换为Position(位置)
  
 struct Node{
     int data;
     Position Next;
 };

这里的List和Position有什么区别呢?可能很多人会有这个疑问,区别在于,List指表头,Position指某个单元,在后面代码中我们会有更清晰的认识,走吧,咱们继续。

现在我们来一一讨论针对链表的各个操作函数。这里多说一句,关于函数返回值的问题:因为C语言没有bool类型,也就是TRUE和FALSE,所以它用1表示真,0表示假。

(你们不要嫌我啰嗦……)

首先,判断某个表是否为空

 /*如果某个表为空,返回1*/

 int IsEmpty(List L){

     return L->Next==NULL;//在最后一个单元里,后面是封口的,也就是指针域是NULL

 }

我们还需要判断某个表是不是在末尾,这是为了作为循环的终止条件。写法上和判空没什么区别,只是分开写会更方便理解,在完整的代码里我们会感受到的,拭目以待吧。

  
 /*如果P是在表中的末尾位置,返回1*/
 int IsLast(Position P) {
     return P->Next==NULL;
 }
  

这些小零碎写完之后,我们就需要把大的零件写出来了(看吧,在咱们这个领域,要搞一件工程,无论说创造零件还是拼装零件,只需要智力加持,代码就会从手中滑落而出,是不是很优雅2333)

对于大部分线性结构,我们要做的操作大体上分为:增加,删除,查找,遍历这四种,我们先写查找,因为这是删除的基础。为什么删除之前要先查找呢?两个原因,1.我们一般是告诉系统要删除的某个“数据”,也就是节点里data的值,所以要先找到这个值所在的节点是哪个,2.因为这是单链表,如果不拿到要删除元素的前驱,我们可能会丢失整个表。

删除有一个很重要的步骤是释放内存,用free函数,我们刚开始学可能会想当然,觉得直接找到那个元素,然后free一下就好了,那就会造成后面的表全部丢失,这是灾难性的后果。

来写一个查找前驱的函数,它返回一个前驱,假如我们给一个3,他就返回3前面那个表的位置,上面说的原因2决定了写这个函数的必要性,而原因1决定了这个函数还需要一个int型参数。

 Position FindPrevious(int X,List L){

     Position P;                 //声明一个节点指针,并指向头节点(和表头一样)

          P=L;

     while (P!=NULL && P->Next->data!=X) { //P没有走到末尾,同时还没找到给定的X时

         P=P->Next;                  //P向后走

       }           //走到这一步时,说明要么没找到,P=NULL(结尾处),要么找到了,P=前驱的位置

     return P;

 }

第二行用到“与(&&)”操作走了捷径,也就是说,如果“与”运算前半部分为假,结果就自动为假,后半部分不再执行,也就是短路操作,咱们上学期讲过。

按逻辑来说,应该紧接着写删除函数的,正好和FindPrevious相配。但是我想强调一个重要的点,一会再说删除,先说查找函数,这个和上面的区别是:这个返回”当前位置“,上面那个返回”前一个位置“。

 Position Find(int X,List L) {

     Position P;

     P=L->Next;      //和上面对比一下,区别在哪?

     while (P!=NULL && X!=P->data) {

         P=P->Next;

     }

     return P;

 }

这个大体思路和上面一样,但有一个要点,这个函数的起始位置在第一个有效元素,比查找前驱的函数靠后一位,原因好理解吧,这个要查找当前位置,而不是前一个,所以从L->Next开始。

好了,我们该说删除操作了

 void Delete(int X,List L) {

     Position P,Temp;        //申请两个节点,一个用作拉住前驱,一个用作临时变量

     P=FindPrevious(X, L);   //用P拉住X的前驱

     if (!IsLast(P)) {       //确定P不是末尾,否则没法删除(末尾后面什么也没有)

     Temp=P->Next;       //用临时指针拉住当前位置,以便后面直接越过这个节点

     P->Next=Temp->Next; //当前节点的前驱直接指向后继,绕过了当前节点

     free(Temp);         //释放当前节点内存

     Temp->Next=NULL;    //将当前节点的指针“收回来”,脑补一下飞机起落架。

     }

 }

这里面有两个我想说的地方

  • 9,11行用了一个临时指针,是为了怕大家绕晕,其实也可以写成P->Next=P->Next->Next; 不过这样一来就不好理解了,肯定一堆人默默吐槽两个Next是什么鬼啊。
  • 第15行貌似教材里没有,但这是为了防止出现野指针。

下面我们说插入函数,分为从前插入和从后插入,emmmm好污的感觉(捂脸),向前插入的一个特点是,表头位置不变,而向后插入需要不断更新Position。

先说从前插入,分为三步:

      1. 打开冰箱
      2. 把大象放进去
      3. 关上冰箱

(划掉)

其实是:

      1. 分配内存
      2. 向后链接
      3. 向前链接
 void InsertBefore(int X,Position P){

     Position NewNode;            //用一个临时变量,用以“拴住”新单元

     NewNode=(List)malloc(sizeof(struct Node));  //申请内存,List相当于struct Node*

     NewNode->data=X;              //将数据填入新单元

     NewNode->Next=P->Next;        //与后方单元相连

     P->Next=NewNode;              //与前方单元相连,这两行顺序不能反,原因…你们试试就知道了

 }

是这个样子

这种情况数组的元素是逆序的。

再说向后插入

 void PushBack(int X,Position P) {

     Position NewNode;       //声明一个新的节点指针

     NewNode=(Position)malloc(sizeof(struct Node));//分配内存

     NewNode->Next=NULL;     //新节点指针域封闭

     NewNode->data=X;        //数据装填

     while(!IsLast(P))       //通过循环走到整个链表的末尾

         P=P->Next;

     P->Next=NewNode;        //将新节点的地址交给原链表末尾,从尾部链接。

 }

这个更容易理解。

再写一个遍历函数,用于打印所有的元素

 void Traverse(List L){

     while (L->Next!=NULL) {

         printf("%d ",L->Next->data);

         L=L->Next;

     }

     printf("\n");

下面是用于演示的主程序,根据自己的需要随意往里面增减部件吧

 int main(){

     int i;

     List L=(List)malloc(sizeof(struct Node));

     L->Next=NULL;

     printf("Input amount of lists\n");

     scanf("%d",&i);

     while (i--) {

         int n;

         scanf("%d",&n);

         PushBack(n, L);

     }

     Traverse(L);

     printf("Which item would you like to remove?\n");

     scanf("%d",&i);

     Delete(i, L);

     printf("\n");

     printf("The current linked lists are : ");

     Traverse(L);

 }

其实每个程序员都是魔法师,程序和算法就是现代的魔法,努力修炼自己的法术吧。

下一篇写游标实现。

p.s.为了方便大家理解,我会在每一行代码后面写注释

pp.s 数据结构是不依赖于具体实现的,所以教科书里用一般性的ElemType表示数据类型,我这里简单起见,全部用int表示

Single linked List by pointer的更多相关文章

  1. Linux C single linked for any data type

    /************************************************************************** * Linux C single linked ...

  2. Intersection of Two Linked Lists(LIST-2 POINTER)

    Write a program to find the node at which the intersection of two singly linked lists begins. For ex ...

  3. 《数据结构》2.3单链表(single linked list)

    //单链表节点的定义 typedef struct node { datatype data; struct node *next; }LNode,*LinkList; //LNode是节点类型,Li ...

  4. 单链表(Single Linked List)

    链表的结点结构  ┌───┬───┐  │data|next│  └───┴───┘ data域--存放结点值的数据域 next域--存放结点的直接后继的地址(位置)的指针域(链域) 实例:从终端输入 ...

  5. Single linked list by cursor

    有了指针实现看似已经足够了,那为什么还要有另外的实现方式呢?原因是诸如BASIC和FORTRAN等许多语言都不支持指针,如果需要链表而又不能使用指针,那么就必须使用另外的实现方法.还有一个原因,是在A ...

  6. CCI_chapter 2 Linked Lists

    2.1  Write code to remove duplicates from an unsorted linked list /* Link list node */ struct node { ...

  7. Cracking the Coding Interview(linked list)

    第二章的内容主要是关于链表的一些问题. 基础代码: class LinkNode { public: int linknum; LinkNode *next; int isvisit; protect ...

  8. [TS] Implement a doubly linked list in TypeScript

    In a doubly linked list each node in the list stores the contents of the node and a pointer or refer ...

  9. Linked List-1

    链表一直是面试的重点问题,恰好最近看到了Stanford的一篇材料,涵盖了链表的基础知识以及派生的各种问题. 第一篇主要是关于链表的基础知识. 一.基本结构 1.数组回顾 链表和数组都是用来存储一堆数 ...

随机推荐

  1. Jquery第二篇【选择器、DOM相关API、事件API】

    前言 前面已经介绍过了Jquery这门语言,其实就是一个javaScript的库-能够简化我们书写的代码-.本博文主要讲解使用Jquery定位HTML控件[定位控件就是获取HTML的标签],使用Jqu ...

  2. vmware三种网络格式

    网络地址转换(NAT) 这种访问模式指的是虚拟机不占用主机所在局域网的ip,通过使用主机的NAT功能访问局域网和互联网,意味着虚拟机可以访问局域网中的其他电脑,但是其他电脑不知道虚拟机的存在. 使用这 ...

  3. MapReduce中Combiner规约的作用以及不能作为MR标配的原因

    作用:在Mapper端对数据进行Combine归约处理,Combine业务逻辑与Reducer端做的完全相同.处理后的数据再传送到Reducer端,再做一次归约.这样的好处是减少了网络传输的数量.在M ...

  4. Win7 32位系统下Sublime text 3的安装以及配置C/C++、java、python的开发环境方法

    本人初学者,此文仅是对这几天鼓捣subime text 3一点微不足道的经验总结,如有明显错误,欢迎指正! 好了,废话少说,进入正题,之前编程java一直用的是eclipse,java的主流IDE,后 ...

  5. String Problem hdu 3374 最小表示法加KMP的next数组

    String Problem Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)To ...

  6. spring框架总结(03)重点介绍(Spring框架的第二种核心掌握)

    1.Spring的AOP编程 什么是AOP?  ----- 在软件行业AOP为Aspect Oriented Programming  也就是面向切面编程,使用AOP编程的好处就是:在不修改源代码的情 ...

  7. JDFS:一款分布式文件管理系统,第五篇(整体架构描述)

    一 前言 截止到目前为止,虽然并不完美,但是JDFS已经初步具备了完整的分布式文件管理功能了,包括:文件的冗余存储.文件元信息的查询.文件的下载.文件的删除等.本文将对JDFS做一个总体的介绍,主要是 ...

  8. 对python编程的初步理解

    一直以来零零散散有听过python,这周终于下定决心学python了.在网上了买个套视频教程,内容分周次学习,有详细的讲解.本人觉得非常好.这里谈谈一下第一周的学习的笔记.望路过的大神给予指正,不胜感 ...

  9. C语言程序设计第一作业

    C语言程序设计第一作业 实验总结 (一) 1.题目:输入圆的半径,求圆周长和面积 2.流程图: 3.测试数据及运行结果: 4.实验分析: 问题1: 出现了错误 原因:是在赋值那写反了 解决方法:应该是 ...

  10. NPOI导出WPF DataGrid控件显示数据

    最近做个项目,需要导出DataGrid显示的数据,中间遇到了不少的坑,在此纪录一下,方便以后查看,也希望能给用到的人,一点帮助. 导出DataGrid显示的数据,并不是导出DataGrid的Items ...