1 线性表

来源:互联网 发布:支付宝系统数据库设计 编辑:程序博客网 时间:2024/06/10 16:57

1 抽象数据类型

  • 理解抽象数据类型
    • 我们对已有的数据类型进行抽象,就有了抽象数据类型。
    • 抽象数据类型(Abstract Data Type,ADT)是指一个数学模型及定义在该模型上的一组操作。
    • 抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关
    • 比如1+1=2这样一个操作,在不同CPU的处理上可能不一样,但由于其定义的数学特性相同,所以在计算机编程者看来,它们都是相同的。“抽象”的意义在于数据类型的数学抽象特性
  • 为了便于在之后的讲解中对抽象数据类型进行规范的描述,我们给出了描述抽象数据类型的标准格式
    ADT 抽象数据类型名
    Data
    数据元素之间逻辑关系的定义
    Operation
    操作
    endADT

2 线性表

2.1 定义(略看)

  • 线性表(List):由零个或多个数据元素组成的有限序列。

    • 首先它是一个序列,也就是说元素之间是有个先来后到的(有顺序),像刚才的小蝌蚪就没有顺序。

    • 若元素存在多个,则第一个元素无前驱,而最后一个元素无后继其他元素都有且只有一个前驱和后继

    • 另外,线性表强调是有限的,事实上无论计算机发展到多强大,它所处理的元素都是有限的。

  • 若将线性表记为(a1,…,ai-1,ai,ai+1,…an),则表中ai-1领先于ai,ai领先于ai+1,称ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素。
    这里写图片描述

  • 线性表元素的个数n(n>=0)定义为线性表的长度,当n=0时,称为空表,记为()。

  • 在不同的问题中,数据元素代表的具体含义不同:

    • 线性表L1: (12, 58, 45, 2, 45, 46), 其元素为数字;
    • 线性表L2: (a, g, r, d, s, t), 其元素为字母。
  • 表格术语:
    这里写图片描述

    • 每个学生的学号﹑姓名﹑性别﹑成绩构成一个数据元素
    • 这种由若干数据项构成的数据元素常称为记录,含有大量记录的线性表称为文件

2.2 线性表的抽象数据类型定义

  • ADT 线性表(List)
  • Data

    • 线性表的数据对象集合为{a1,a2,…,an},每个元素的类型均为DataType(//整型、浮点…)。其中,除第一个元素a1外,每 一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个直接后继元素。 数据元素之间的关系是一对一的关系。
  • Operation(//建判清增删查长

    • InitList(*L): 初始化操作,建立一个空的线性表L。
    • ListEmpty(L): 判断线性表是否为空表,若线性表为空,返回true,否则返回false。
    • ClearList(*L): 将线性表清空。
    • GetElem(L,i,*e): 将线性表L中的第i个位置(//从1开始)元素值返回给e。
    • LocateElem(L,e): 在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中序号;否则,返回0表示失败。若满足条件的元素不惟一,则返回最小的位序。(//从1开始)
    • ListInsert(*L,i,e): 在线性表L中第i个位置插入新元素e。
    • ListDelete(*L,i,*e): 删除线性表L中第i个位置元素,并用e返回其值。
    • ListPrior (L, x):求前驱函数。当x在线性表L中,且其位序大于1,则函数值为x的直接前驱,否则为空元素。
    • ListNext (L, x):求后继函数。当x在线性表L中,且其位序小于Length(L), 则函数值为x的直接后继,否则为空元素。
    • ListLength(L): 返回线性表L的元素个数。
  • endADT

对于不同的应用,线性表的基本操作是不同的,上述操作是最基本的,对于实际问题中涉及的关于线性表的更复杂操作,完全可以用这些基本操作的组合来实现

  • 例子:比如要实现两个线性表A、B的并集操作,即要使得集合A=A∪B。

    解析:我们只需要循环遍历集合B中的每个元素,判断当前元素是否存在A中,弱不存在,则插入A中即可。

    需要运用到几个基本的操作组合即可:
    ListLength(L);
    GetElem(L,i,*e);
    LocateElem(L,e);
    ListInsert(*L,i,e);

    //union表示并集, La表示A集合, Lb表示B集合。void unionL(List *La, list Lb){  int La_len, Lb_len, i;  ElemType e;  La_len = ListLength(*La);  Lb_len = ListLength(Lb);  for( i=1; i <= Lb_len; i++ )  {      GetElem(Lb, i, &e);      if( !LocateElem(*La, e) )      {          ListInsert(La, ++La_len, e);      }  }} 

2.3 线性表的两种存储结构

2.3.1 顺序存储结构

  • 1.定义

    • 用一段地址连续的存储单元依次存储线性表的数据元素(a1,a2,…,an)。(类似数组)

      这里写图片描述

    • 代码:

      #define MAXSIZE 20    typedef int ElemType;  //可以不是int类型!!!ElemType代表线性表元素类型typedef struct{  ElemType data[MAXSIZE];  int length;    // 线性表当前长度} SqList;//我们封装了一个结构(事实上就是数组),增加了当前长度的变量。
    • 顺序存储结构三个属性:

      1. 存储空间的起始位置,数组data,它的存储位置就是线性表存储空间的存储位置。
      2. 线性表的最大存储容量:数组的长度MaxSize
      3. 线性表的当前长度:length

        附:线性表的当前长度是线性表中元素的个数,是会变化的。(数组不变)

  • 2.定位操作

    这里写图片描述

    地址计算公式:

    假设ElemType占用的是c个存储单元(字节),那么线性表中第i+1个数据元素和第i个数据元素的存储位置的关系是(LOC表示获得存储位置的函数): LOC(ai+1) = LOC(ai) + c

    第i个数据元素ai的存储位置可以由a1推算得出: LOC(ai) = LOC(a1) + (i-1)*c

    通过这个公式,我们可以随时计算出线性表中任意位置的地址,它的存储时间性能为0(1)

    • 实现GetElem(SqList L, int i, ElemType *e),即将线性表L中的第i个位置元素值返回(从1开始)。就程序而言只需要把数组第i-1下标的值返回即可(从0开始)。

    • 代码:

      #define OK 1#define ERROR 0#define TRUE 1#define FALSE 0typedef int Status;// 注意:这里返回值Status是一个整型,约定返回1代表OK,返回0代表ERROR。// 初始条件:顺序线性表L已存在,1 <= i <= ListLength(L)// 操作结果:用ElemType型指针e返回L中第i个数据元素的值。Status GetElem(SqList L, int i, ElemType *e){    if( L.length==0 || i<1 || i>L.length )    {        return ERROR;    }    *e = L.data[i-1];    return OK;}
  • 3.插入操作

    • 实现ListInsert(*L, i, e),即在线性表L中的第i个位置插入新元素e

    • 所以插入算法的思路:

      1. 如果插入位置不合理,抛出异常;
      2. 如果线性表长度大于等于数组长度,则抛出异常或动态增加数组容量;
      3. 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
      4. 将要插入元素填入位置i处;
      5. 线性表长+1。
  • 代码:

    /* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L)。 *//* 操作结果:在L中第i个位置之前插入新的数据元素e,L长度+1。*/Status ListInsert(SqList *L, int i, ElemType e){    int k;    if( L->length == MAXSIZE )  // 顺序线性表已经满了    {        return ERROR;    }    if( i<1 || i>L->length+1)   // 当i不在范围内时    {        return ERROR;    }    if( i <= L->length )   // 若插入数据位置不在表尾    {        /* 将要插入位置后数据元素向后移动一位 */        for( k=L->length-1; k >= i-1; k-- )        {            L->data[k+1] = L->data[k];        }    }    L->data[i-1] = e;  // 将新元素插入    L->length++;    return OK;}
  • 4.删除操作

    • 删除算法的思路:(直接覆盖)

      1. 如果删除位置不合理,抛出异常;
      2. 取出删除元素;
      3. 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
      4. 表长-1。
    • 代码:

      /* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) *//* 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度-1 */Status ListDelete(SqList *L, int i, ElemType *e){    int k;    if( L->length == 0 )    {        return ERROR;    }    if( i<1 || i>L->length )    {        return ERROR;    }    *e = L->data[i-1];    if( i < L->length )    {        for( k=i; k < L->length; k++ )        {            L->data[k-1] = L->data[k];        }    }    L->length--;    return OK;}
  • 插入和删除的时间复杂度。
    最好的情况:插入和删除操作刚好要求在最后一个位置,时间复杂度为O(1)。
    最坏的情况:如果要插入和删除的位置是第一个元素,时间复杂度为O(n)。
    至于平均情况,就取中间值O((n-1)/2)。简化后还是O(n)。

  • 线性表顺序存储结构的优缺点:

    • 优点:
      1. 无须为表示表中元素之间的逻辑关系而增加额外的存储空间。(相对链式)
      2. 可以快速地存取表中任意位置的元素。
    • 缺点:

      1. 插入和删除操作需要移动大量元素。
      2. 当线性表长度变化较大时,难以确定存储空间的容量。
      3. 容易造成存储空间的“碎片”。
    • 比较适合元素个数比较稳定,不经常插入和删除元素,而存取更多数据的应用。

2.3.2 链式存储结构

  • 1.定义

    • 存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称为指针或链。这两部分信息组成数据元素称为存储映像,称为结点(Node)。我们把链表中的第一个结点的存储位置叫做头指针,最后一个结点指针为空(NULL)。

      这里写图片描述

      这里写图片描述

    • 链表的每个结点中只包含一个指针域,所以叫做单链表

    • 头结点和头指针:

      这里写图片描述

      这里写图片描述

      ☆头指针

      1. 头指针指向链表第一个结点,若链表有头结点,则指向头结点
      2. 头指针具有标识作用,所以常用头指针冠以链表的名字(指针变量的名字)。
      3. 无论链表是否为空,头指针不为空。头指针是链表的必要元素。

      ☆头结点

      1. 头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(但也可以用来存放链表的长度)。
      2. 有了头结点,对在第一元素结点前插入结点和删除第一结点前操作与其它结点的操作就统一了。(便于第一个节点的插入删除操作统一
      3. 头结点不必要
    • 单链表代码:(单链表是链式存储

      typedef struct Node  //结点{    ElemType data;      // 数据域    struct Node* Next;  // 指针域} Node;typedef struct Node* LinkList;

      或者
      这里写图片描述

    • 单链表存储结构

      假设p是指向线性表第i个元素的指针,则该结点ai的数据域我们可以用p->data的值是一个数据元素,结点ai的指针域可以用p->next来表示,p->next的值是一个指针。

      如果p->data = ai,那么p->next->data = ai+1。

  • 2.单链表定位

    ☆在单链表中,第i个元素必须得从第一个结点开始挨个儿找。

    • 获得链表第i个数据的算法思路:
      1. 声明一个结点p指向链表第一个结点,初始化j从1开始;
      2. 当j < i时,就遍历链表,让p的指针向后移动,不断指向一下结点,j+1;
      3. 若到链表末尾p为空,则说明第i个元素不存在;
      4. 否则查找成功,返回结点p的数据。
  • 代码:

    /* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) *//* 操作结果:用e返回L中第i个数据元素的值 */Status GetElem( LinkList L, int i, ElemType *e ){    int j;    LinkList p;   //声明结点!    p = L->next;  //初始化指向链表第一个结点(L为头指针,指向头结点,->next后指向第一个结点)    j = 1;    while( p && j<i )    {        p = p->next;        ++j;    }    if( !p || j>i )    {        return ERROR;  //代表0,没找到    }    *e = p->data;    return OK;   //代表1,查找成功}//for和while:由于单链表的结构中没有定义表长,所以不能实现知道要循环多少次,因此也就不方便使用for来控制循环。
  • 2.单链表插入

    这里写图片描述

    • 注意:

      先s->next = p->next;
      再p->next = s;

    • 单链表第i个数据插入结点的算法思路:

      1. 声明一结点p指向链表头结点,初始化j从1开始;
      2. 当j<1时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
      3. 若到链表末尾p为空,则说明第i个元素不存在;
      4. 否则查找成功,在系统中生成一个空结点s;
      5. 将数据元素e赋值给s->data;
      6. 单链表的插入s->next = p->next; p->next = s;
      7. 返回成功。
    • 代码:

      /* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) *//* 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1 */Status ListInsert(LinkList *L, int i, ElemType e){    int j;    LinkList p, s;    p = *L;    j = 1;    while( p && j<i )   // 用于寻找第i个结点    {        p = p->next;        j++;    }    if( !p || j>i )    {        return ERROR;    }    s = (LinkList)malloc(sizeof(Node));  //动态申请内存    s->data = e;    s->next = p->next;    p->next = s;    return OK;}
  • 3.单链表删除

    这里写图片描述

    • 思路:假设元素a2的结点为q,要实现结点q删除单链表的操作,其实就是将它的前继结点的指针绕过指向后继结点即可。

    • 代码:

      /* 初始条件:顺序线性表L已存在,1<=i<=ListLength(L) *//* 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度-1 */Status ListDelete(LinkList *L, int i, ElemType *e){    int j;    LinkList p, q;    p = *L;    j = 1;    while( p->next && j<i )      {        p = p->next;        ++j;    }    //跳出循环的时候p指向第i-1个结点    if( !(p->next) || j>i )    {        return ERROR;    }    q = p->next;  //这一步为了找到q,返回其值,并将q释放。                  //否则上下合成一步:p->next = p->next->next;    p->next = q->next;    *e = q->data;   //为了返回删除元素值    free(q);   //return OK;}
  • 插入和删除的时间复杂度。

    单链表插入和删除,都是先遍历查找第i个元素,再插入和删除元素。时间复杂度是O(n)。

  • 4.单链表整表创建

    顺序存储结构的线性表的整表创建,我们可以用数组的初始化来直观理解。

    单链表和顺序存储结构就不一样了,它的数据分散在内存各个角落,他的增长也是动态的。

    对于每个链表来说,它所占用空间的大小和位置是不需要预先分配划定的,可以根据系统的情况和实际的需求即时生成。

    • 创建单链表的过程是一个动态生成链表的过程,从“空表”的初始状态起,依次建立各元素结点并逐个插入链表。

    • 单链表整表创建的算法思路如下:

      1. 声明一结点p和计数器变量i;
      2. 初始化一空链表L;
      3. 让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
      4. 循环实现后继结点的赋值和插入。
    • 两种单链表创建法:头插法,尾插法

      • 头插法:把新加进的元素放在表头后的第一个位置:

        1. 先让新节点的next指向头节点之后
        2. 然后让表头的next指向新节点

          /* 头插法建立单链表示例 */void CreateListHead(LinkList *L, int n){    LinkList p;    int i;    srand(time(0));   // 初始化随机数种子    *L = (LinkList)malloc(sizeof(Node));    (*L)->next = NULL;    for( i=0; i < n; i++ )    {        p = (LinkList)malloc(sizeof(Node));  // 生成新结点,p为临时结点        p->data = rand()%100+1;  //1~100        p->next = (*L)->next;        (*L)->next = p;    }}

          头插法建立链表虽然算法简单,但生成的链表中结点的次序和输入的顺序相反。

      • ☆尾插法:把新结点都插入到最后:

        /* 尾插法建立单链表演示 */void CreateListTail(LinkList *L, int n){    LinkList p, r;    int i;    srand(time(0));    *L = (LinkList)malloc(sizeof(Node));    r = *L;    for( i=0; i < n; i++ )    {        p = (Node *)malloc(sizeof(Node));        p->data = rand()%100+1;        r->next = p;           // 经过赋值后p为尾结点        r = p;                 // r继续作为尾结点    }    r->next = NULL;            //最后结点指向NULL}
  • 5.单链表的整表删除

    • 单链表整表删除的算法思路如下:(三步)

      1. 声明结点p和q;
      2. 将第一个结点赋值给p,下一结点赋值给q;
      3. 循环执行释放p和将q赋值给p的操作;
    • 代码:

      Status ClearList(LinkList *L){    LinkList p, q;    p = (*L)->next;    while(p)    {        q = p->next;        free(p);        p = q;    }    (*L)->next = NULL;    return OK;}
  • 6.快慢指针-找中点

    • 题目:快速找到未知长度单链表的中间节点。

    • 普通方法:首先遍历一遍链表以确定链表的长度L。然后再次从头节点出发循环L/2次找到单链表的中间节点。算法复杂度为:O(L+L/2)=O(3L/2)。

    • 快慢指针原理:设置两个指针search、*mid都指向单链表的头节点。其中 search的移动速度是*mid的2倍。当*search指向末尾节点的时候,mid正好就在中间了。这也是标尺的思想。算法复杂度为:O(L/2)。

    • 快慢指针程序:

    Status GetMidNode(LinkList L, ElemType *e){    LinkList search, mid;    mid = search = L;    while (search->next != NULL)    {        //search移动的速度是 mid 的2倍        if (search->next->next != NULL)        {            search = search->next->next;            mid = mid->next;        }        else        {            search = search->next;  //还有一个元素才指向末尾的情况        }    }    *e = mid->data;    return OK;}

    7.单链表结构与顺序存储结构优缺点PK:(时间性能、空间性能)

    • 时间性能
    • 查找

      顺序存储结构O(1),单链表O(n)

    • 插入和删除

      顺序存储结构需要平均移动表长一半的元素,时间为O(n)

      单链表在计算出某位置的指针后,插入和删除时间仅为O(1)

      • ☆ 如果在我们不知道第i个元素的指针位置,单链表数据结构在(单次)插入和删除操作上,与线性表的顺序存储结构是没有太大优势的。

      • 但如果,我们希望从第i个位置开始,插入连续10个元素,对于顺序存储结构意味着,每一次插入都需要移动n-i个位置,所以每次都是O(n)。而单链表,我们只需要在第一次时,找到第i个位置的指针,此时为O(n),接下来只是简单地通过赋值移动指针而已,时间复杂度都是O(1)。

    • 空间性能

      顺序存储结构需要预分配存储空间,分大了,容易造成空间浪费,分小了,容易发生溢出。

      单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制。

    • 总结

      若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。

      若需要频繁插入和删除时,宜采用单链表结构。

2.3.3 静态链表

用数组描述的链表叫做静态链表,这种描述方法叫做游标实现法。(游标找下标,游标相当指针,下标相当地址)

这里写图片描述

  1. 线性表的静态链表存储结构

    #define MAXSIZE 1000 typedef struct{   ElemType data;  // 数据   int cur;        // 游标(Cursor)} Component, StaticLinkList[MAXSIZE]; //一个结构体代表一个链表的结点,数组存放来表示整个链表
  2. 静态链表进行初始化相当于初始化数组:

    Status InitList(StaticLinkList space){   int i;   for( i=0; i < MAXSIZE-1; i++ )   space[i].cur = i + 1;  //初始化游标cur(相当指针)指向下一个数组元素地址   space[MAXSIZE-1].cur = 0;  //开始时数组末位指向0地址,相当头指针,0地址相当头结点   return OK;}
    • 备注:

      • 数组的第一个和最后一个元素,他们的data不存放数据。

      • 通常把未使用的数组元素称为备用链表。

      • 数组的第一个元素,即下标为0的元素的cur(游标)存放备用链表的第一个结点的下标。

      • 数组的最后一个元素,即下标为MAXSIZE-1的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点作用。

  3. 静态链表的插入操作

    • 在动态链表中,结点的申请和释放分别借用C语言的malloc()和free()两个函数来实现。

    • 在静态链表中,操作的是数组,不存在像动态链表的结点申请和释放的问题,所以我们需要自己实现这两个函数。

    • 为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的用游标链成一个备用链表。每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。

      这里写图片描述

      这里写图片描述

    • 代码:

      //首先是获得空闲分量的下标:int Malloc_SLL(StaticLinkList space){    int i = space[0].cur;  //数组第一个下标指向空闲第一个元素(见图,现在指向下标6)    if( space[0].cur )    space[0].cur = space[i].cur;   // 把它的下一个下标为7的地址用来作为第一个空闲元素。    return i;}/* 在静态链表L中第i个元素之前插入新的数据元素e */Status ListInsert( StaticLinkList L, int i, ElemType e ){    int j, k, l;    k = MAX_SIZE - 1;    // 数组的最后一个 元素!    if( i<1 || i>ListLength(L)+1 )    {        return ERROR;    }    j = Malloc_SLL(L);      //获取第一个空闲元素下标5:(看第一幅图)    if( j )    {        L[j].data = e;     // 插入元素        //游标修改        for( l=1; l <= i-1; l++ )     //例子i=2        {            k = L[k].cur;   //遍历所有元素找到插入位置,游标cur 1        }        L[j].cur = L[k].cur;  //空闲元素的游标cur=下标1的cur 2        L[k].cur = j;    //下标1的游标cur=5        return OK;    }    return ERROR;}
  4. 静态链表的删除操作

    这里写图片描述

    这里写图片描述

    • 注意点:

      • 删除后要将下标为k的空闲结点回收到备用链表
    • 代码:

      /* 删除在L中的第i个数据元素 */Status ListDelete(StaticLinkList L, int i){    int j, k;    if( i<1 || i>ListLength(L) )    {        return ERROR;    }    k = MAX_SIZE - 1;      for( j=1; j <= i-1; j++ )  //i=3    {        k = L[k].cur;    // k1 = 1, k2 = 5    }    j = L[k].cur;        // j = 2    L[k].cur = L[j].cur;    Free_SLL(L, j);    return OK;}/* 将下标为k的空闲结点回收到备用链表 */void Free_SLL(StaticLinkList space, int k){    space[k].cur = space[0].cur;    space[0].cur = k;}/* 返回L中数据元素个数 */int ListLength(StaticLinkList L){    int j = 0;    int i = L[MAXSIZE-1].cur;    while(i)    {        i = L[i].cur;        j++;    }    return j;}
  5. 静态链表优缺点总结

    • 优点:
      在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点。

    • 缺点:
      没有解决连续存储分配(数组)带来的表长难以确定的问题。
      失去了顺序存储结构随机存取的特性。

    静态链表其实是为了给没有指针的编程语言设计的一种实现单链表功能的方法。

2.3.4 循环链表

这里写图片描述

将单链表中终端结点的指针端由空指针改为指向头结点,这种头尾相接的单链表成为单循环链表,简称循环链表。

注1:这里并不是说循环链表一定要有头结点

循环链表的定义、初始化、插入、删除、查询、遍历:

```#include <stdio.h>#include <stdlib.h>/*链表存储结构的定义*/typedef struct CLinkList{    int data;    struct CLinkList *next;}node;/************************************************************************//* 操作                                                                  *//************************************************************************//*初始化循环链表*/void ds_init(node **pNode){    int item;    node *temp;    node *target;    printf("输入结点的值,输入0完成初始化\n");    while(1)    {        scanf("%d", &item);        fflush(stdin);        if(item == 0)            return;        if((*pNode) == NULL)        { /*循环链表中只有一个结点*/            *pNode = (node*)malloc(sizeof(struct CLinkList));            if(!(*pNode))                exit(0);            (*pNode)->data = item;            (*pNode)->next = *pNode;        }        else        {            /*找到next指向第一个结点的结点*/            for(target = (*pNode); target->next != (*pNode); target = target->next)                ;            /*生成一个新的结点*/            temp = (node *)malloc(sizeof(struct CLinkList));            if(!temp)                exit(0);            temp->data = item;            temp->next = *pNode;            target->next = temp;        }    }}/*插入结点*//*参数:链表的第一个结点,插入的位置*/void ds_insert(node **pNode , int i){    node *temp;    node *target;    node *p;    int item;    int j = 1;    printf("输入要插入结点的值:");    scanf("%d", &item);    if(i == 1)    { //新插入的结点作为第一个结点        temp = (node *)malloc(sizeof(struct CLinkList));        if(!temp)            exit(0);        temp ->data = item;        /*寻找到最后一个结点*/        for(target = (*pNode); target->next != (*pNode); target = target->next)            ;        temp->next = (*pNode);        target->next = temp;        *pNode = temp;    }    else    {        target = *pNode;        for( ; j < (i-1); ++j )        {            target=target->next;        }        temp = (node *)malloc(sizeof(struct CLinkList));        if(!temp)            exit(0);        temp ->data = item;        p = target->next;        target->next = temp;        temp->next = p;    }}/*删除结点*/void ds_delete(node **pNode, int i){    node *target;    node *temp;    int j = 1;    if(i == 1)    { //删除的是第一个结点        /*找到最后一个结点*/        for(target = *pNode; target->next != *pNode;target = target->next)            ;        temp = *pNode;        *pNode = (*pNode)->next;        target->next = *pNode;        free(temp);    }    else    {        target = *pNode;        for( ; j < i-1; ++j )        {            target = target->next;        }        temp = target->next;        target->next = temp->next;        free(temp);    }}/*返回结点所在位置*/int ds_search(node *pNode, int elem){    node *target;    int i = 1;    for(target = pNode; target->data != elem && target->next != pNode; ++i)    {        target = target->next;    }    if(target->next == pNode) /*表中不存在该元素*/        return 0;    else        return i;}/*遍历*/void ds_traverse(node *pNode){    node *temp;    temp = pNode;    printf("***********链表中的元素******************\n");    do    {        printf("%4d ", temp->data);    }while((temp = temp->next) != pNode);    printf("\n");}int main(){    node *pHead = NULL;    char opp;    int find;    printf("1.初始化链表 \n\n2.插入结点 \n\n3.删除结点 \n\n4.返回结点位置 \n\n5.遍历链表  \n\n0.退出 \n\n请选择你的操作:");    while(opp != '0')    {        scanf("%c", &opp);        switch(opp)        {            case '1':                ds_init(&pHead);                printf("\n");                ds_traverse(pHead);                break;            case '2':                printf("输入需要插入结点的位置?");                scanf("%d",  &find);                ds_insert(&pHead, find);                printf("在位置%d插入值后:\n",  find);                ds_traverse(pHead);                printf("\n");                break;            case '3':                printf("输入需要删除的结点位置?");                scanf("%d",  &find);                ds_delete(&pHead, find);                printf("删除第%d个结点后:\n",  find);                ds_traverse(pHead);                printf("\n");                break;            case '4':                printf("你要查找倒数第几个结点的值?");                scanf("%d",  &find);                printf("元素%d所在位置:%d\n",  find,  ds_search(pHead, find));                //ListTraverse(L);                printf("\n");                break;            case '5':                ds_traverse(pHead);                printf("\n");                break;            case '0':                exit(0);        }    }    return 0;}```

有时为了简化某些操作在链表中设立尾指针,而不是头指针。例如,将两个用循环链表存储的线性表合并成一个线性表,此时仅需将一个表的表尾和另一个表的表头相连即可。

2.3.5 双向循环链表

这里写图片描述

  • 在双向链表的结点中应有两个指针域,一个指向直接后继,一个指向直接前驱。

    typedef  struct dnode{datatype  data;struct  dnode  *prior;struct  dnode  *next;}DNode, *DList;
  • 双向链表的插入和删除与单链表不同

    • 删除操作指针变化

      这里写图片描述

    • p->prior->next=p->next;
      p->next->prior=p->prior;
      free(p);

    • 插入操作指针变化

      这里写图片描述

    • s->prior=p->prior;
      p->prior->next=s;
      s->next=p;
      p->prior=s;

原创粉丝点击