• 线性表(代码分解)


    完整篇代码链接:https://www.cnblogs.com/ouyang_wsgwz/p/7695713.html

    线性表的基本操作

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

    ADT List {

      数据对象:D={ a| a∈ ElemSet, i = 1, 2, ... , n, n≥0 }

      数据关系:R1={ < ai-1 ,ai > |  ai-1 , a∈ D,  i = 2, ... , n }

     

      基本操作

        InitList( &L )

          操作结果:构造一个空的线性表 L 。

        DestroyList( &L )

          初始条件:线性表 L 已存在。

          操作结果:销毁线性表 L 。
        ClearList( &L )

          初始条件:线性表 L 已存在。

          操作结果:将 L 重置为空表。

        ListEmpty( L )

          初始条件:线性表L已存在。

          操作结果:若 L 为空表,则返回 TRUE,否则返回 FALSE。

        ListLength( L )

          初始条件:线性表 L 已存在。

          操作结果:返回 L 中元素个数。

        GetElem( L, i, &e )
          初始条件:线性表 L 已存在,1≤i≤LengthList(L)。

          操作结果:用 e 返回 L 中第 i 个元素的值。

        LocateElem( L, e, compare( ) )

          初始条件:线性表 L 已存在,compare( ) 是元素判定函数。

          操作结果:返回 L 中第1个与 e 满足关系 compare( ) 的元素的位序。若这样的元素不存在,则返回值为0。

        PriorElem( L, cur_e, &pre_e )
          初始条件:线性表 L 已存在。

          操作结果:若 cur_e 是 L 中的数据元素,则用 pre_e 返回它的前驱,否则操作失败,pre_e 无定义。

        NextElem( L, cur_e, &next_e )

          初始条件:线性表 L 已存在。

          操作结果:若 cur_e 是 L 中的数据元素,则用 next_e 返回它的后继,否则操作失败,next_e 无定义。

        ListInsert( &L, i, e )

          初始条件:线性表 L 已存在,1≤i≤LengthList(L) + 1。

          操作结果:在 L 的第 i 个元素之前插入新的元素 e,L 的长度增1。

        ListDelete( &L, i, &e )

          初始条件:线性表 L 已存在且非空,1≤i≤LengthList(L)。

          操作结果:删除 L 的第 i 个元素,并用 e 返回其值,L 的长度减1。

        ListTraverse(L, visit( ))

          初始条件:线性表 L 已存在,visit( ) 为元素的访问函数。

          操作结果:依次对 L 的每个元素调用函数 visit( )。一旦 visit( ) 失败,则操作失败。

    } ADT List

    对上述定义的抽象数据类型线性表,还可以进行一些更复杂的操作。

      例如将两个线性表合并成一个线性表:

      1.新集合 A = A U B

     1 void union(List &La, List &Lb) {
     2     // 将所有在线性表Lb中但不在La中的数据元素插入到La中
     3 
     4     //求线性表的长度
     5     La_len = ListLength(La);
     6     Lb_len = ListLenght(Lb);
     7 
     8     for (i = 1; i <= Lb_len; i++) {
     9         GetElem(Lb, i, e);            //取Lb中第i个数据元素赋给e
    10         if (!LocateElem(La, e, equal))
    11             ListInsert(La, ++La_len, e);
    12             //La中不存在和e相同的数据元素,则插之
    13     }
    14 }// union
    View Code

      2.新的线性表 LC = LA + LB ,其中LA, LB, LC,都是非递减有序排列

     1 void MergeList(List La, List Lb, List &Lc) {
     2     // 已知线性表La和Lb中的数据元素按值非递减排列。
     3     // 归并La和Lb得到新的线性表Lc, Lc的数据元素也按值非递减排列。
     4 
     5     InitList(Lc);
     6     i = j = 1;    k = 0;
     7     La_len = ListLength(La); Lb_ListLength(Lb);
     8     while ((i <= La_len) && (i <= Lb_len)) { // La和Lb均非空
     9         GetElem(La, i, ai); GetElem(Lc, ++k, bj);
    10         if (ai <= bj) {
    11             ListInsert(Lc, ++k, ai);
    12             ++i;
    13         }
    14         else {
    15             ListIsnert(Lc, ++k, bj);
    16             ++j;
    17         }
    18         
    19         while (i <= La_len) {
    20             GetElem(La, i++, ai);
    21             ListInsert(Lc, ++k, ai);
    22         }
    23         while (j <= Lb_len) {
    24             GetElem(Lb, j++, bj);
    25             ListInsert(Lc, ++k, bj);
    26         }
    27     }
    28 }// MergeList
    View Code

    线性表的顺序表示

    1.静态分配

    1 #define MaxSize 50              //定义线性表的最大长度
    2 typedef struct {
    3     ElemType data[MaxSize];        //顺序表的元素
    4     int length;                    //顺序表的当前长度
    5 }SqList;                        //顺序表的类型定义
    View Code

    2.动态分配

    1 #define InitSize 100        //表长度的初始定义
    2 typedef struct {            
    3     ElemType *data;            //指示动态分配数组的指针
    4     int MaxSize, length;    //数组的最大容量和当前个数
    5 } SeqList;                    //动态分配数组顺序表的类型定义
    View Code

    顺序表上基本操作的实现

    1.插入操作(将元素e插入到顺序表L中第i个位置)

     1 bool ListInsert(SqList &L, int i, ElemType e) {
     2     if (i < 1 || i > L.length + 1)            //判断i的范围是否有效
     3         return false;
     4     if (L.length >= MaxSize)                //当前存储空间已满,不能插入
     5         return false;
     6     for (int j = L.length; j >= i; j--)        //将第i个元素及之后的元素后移
     7         L.data[j] = L.data[j - 1];    
     8     L.data[i - 1] = e;                        //在位置i处放入e
     9     L.length++;                                //线性表长度加1
    10     return true;
    11 }
    View Code

      最好情况:在表尾插入(即 i = n+1),元素后移语句将不执行,时间复杂度为O(1)。

      最坏情况:在表头插入(即 i = 1),元素后移语句将执行n次,时间复杂度为O(n)。

      平均情况:假设pi (pi = 1/(n+1))是在第i个位置上插入一个结点的概率,则在长度为n的线性表中插入一个结点时,所需移动的结点的平均次数为

        

       因此,线性表插入算法的平均时间复杂度为O(n)。

    2.删除操作(实现删除顺序表L中第i个位置的元素)

     1 bool ListDelete(SqList &L, int i, ElemType &e) {
     2     if (i < 1 || i > L.length)
     3         return false;
     4     e = L.data[i - 1];;
     5     for (int j = i; j < L.length; j++) {
     6         L.data[j - 1] = L.data[j];
     7     }
     8     L.length--;
     9     return true;
    10 }
    View Code

    同上,时间复杂度也为O(n)

    3.按值查找(查找顺序表中值为e的元素,如果查找成功,返回元素位序,否则返回0)

    1 int LocateElem(SqList L, ElemType e) {
    2     int i;
    3     for (i = 0; i < L.length; i++)
    4         if (L.data[i] == e)
    5             return i + 1;
    6     return 0;                            //退出循环查找失败
    7 }
    View Code

    时间复杂度也为O(n)。

    线性表的链式表示

    单链表(线性表的链式存储)中的结点类型为:

    1 typedef struct LNode {        //定义单链表结点类型
    2     ElemType data;            //数据域
    3     struct LNode *next;        //指针域
    4 } LNode, *LinkList;

    1.采用头插法建立单链表

     1 LinkList List_HeadInsert(LinkList &L) {
     2     LNode *s; int x;
     3     L = (LinkList)malloc(sizeof(LNode));        //创建头结点
     4     L->next = NULL;                                //初始化为空链表
     5     scanf("%d", &x);                            //输入结点的值
     6     while (x != 9999) {                            //输入9999表示结束
     7         s = (LNode *)malloc(sizeof(LNode));        //创建新结点
     8         s->data = x;
     9         s->next = L->next;
    10         L->next = s;                            //将新结点插入表中,L为头指针
    11         sealed("%d", &x);
    12     }
    13     return L
    14 }
    View Code

    采用头插法建立单链表时,读入数据的顺序与生成的链表中的元素的顺序是相反的。每个节点插入的时间为O(1),设单链表长为n,则总时间复杂度为O(n)。

    2.采用尾插法建立单链表

     1 LinkList List_TailInsert(LinkList &L) {
     2     int x;                                    //设元素类型为整型
     3     L = (Link    List)malloc(sizeof(LNode));    
     4     LNode *s, *r = L;                        //r为表尾指针
     5     scanf("%d", &x);                        //输入结点的值
     6     while (x != 9999){                        //输入9999表示结束
     7         s = (LNode *)malloc(sizeof(LNode));
     8         s->data = x;
     9         r->next = s;
    10         r = s;                                //r指向新的表尾结点
    11         scanf("%d", &x);
    12     }
    13     r->next = NULL;                            //尾结点指针置空
    14     return L;
    15 }
    View Code

    时间复杂度跟头插法相同

    3.按序号查找结点值

     (取出单链表L带头结点中的第i个位置的结点指针)

     1 LNode *GetElem(LinkList L, int i) {
     2     int j = 1;
     3     LNode *p = L->next;
     4     if (i == 0) return L;
     5     if (i < 1) return NULL;
     6     while (p && j < i) {
     7         p = p->next;
     8         j++;
     9     }
    10     return p;
    11 }
    View Code

    按序号查找操作的时间复杂度为O(n)

    4.按值查找表结点

    (查找单链表L带头节点中数据域等于e的结点指针,否则返回NULL)

    1 LNode *LocateElem(LinkList L, ElemType e) {
    2     LNode *p = L->next;                    
    3     while (p != NULL && p->data != e)    //从第1个结点开始查找data域为e的结点
    4         p = p->next;
    5     return p;                            //找到后返回该结点指针,否则返回NULL
    6 }
    View Code

    5.插入结点操作

      插入结点操作将值为x的新结点插入到单链表的第i个位置上。代码片段如下:

       p = GetElem(L, i - 1); s->next = p->next; p->next = s; 

      本算法主要的时间开销在于查找第i-1个元素,时间复杂度为O(n)。若在给定的结点后面插入新结点,则时间复杂度仅为O(1)。

      扩展:对某一结点进行前插操作。

    //将*s结点插入到*p之前的主要代码片段
    s->next = p->next;        //修改指针域,不能颠倒
    p->next = s;
    temp = p->data;            //交换数据域部分
    p->data = s->data;
    s->data = temp;

    6.删除结点操作

    1 p = GetElem(L, i - 1);    //查找删除位置的前驱结点
    2 q = p->next;            //令q指向被删除结点
    3 p->next = q->next;        //将*q结点从链中“断开”
    4 free(q);                //释放结点的存储空间

    和插入算法一样。该算法的主要时间也耗费在查找操作上,时间复杂度为O(n)。

      扩展:删除结点*p。

      要删除某个给定的结点*p,通常的做法是先从链表的头结点开始顺序找到其前驱结点,然后再执行删除操作,算法的时间复杂度为O(n)。

      其实,删除结点*p的操作可用删除*p的后继结点操作来实现,实质就是将其后继节点的值赋予其自身,然后删除后继结点,也能使得时间复杂度为O(1)。

      代码片段如下:

    1 q = p->next;                //令q指向*p的后继结点
    2 p->data = p->next->data;    //和后继结点交换数据域
    3 p->next = q->next;            //将*q结点从链中断开
    4 free(q);                    //释放后继结点的存储空间

    7.求表长操作

      求表长操作就是计算单链表种数据结点(不含头结点)的个数,需要从第一个结点开始顺序依次访问表中的每个结点,在访问时,计数器变量加1,直到空结点为止。算法的时间复杂度为O(n)。

     

    双链表

    双链表中结点类型的描述如下:

    typedef struct DNode {                //定义双链表结点类型
        ElemType data;                    //数据域
        struct DNode *prior, *next;        //前驱和后继指针
    }DNode, *DLinklist;

    1.双链表的插入操作

    在双链表中p所指的结点之后插入结点*s,代码片段如下:

      //双链表的插入操作 s->next = p->next; //将结点*s插入到结点*p之后 p->next->prior = s; s->prior = p; p->next = s; 

    2.双链表的删除操作

    代码片段如下:

    1 //p 为 q的前一个节点
    2 //删除q节点
    3 p->next = q->next;    
    4 q->next->prior = p;
    5 free(q);

    注:在建立双链表的操作中,也可采用如同单链表的头插法和尾插法,但在操作上需要注意指针的变化和单链表有所不同。

    循环单链表

      在循环单链表中,表尾结点*r 的next 域指向L,故表中没有之指针域为NULL的结点,因此,循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头结点。

      循环单链表的插入、删除算法与单链表的几乎一样,所不同的是若操作是在表尾进行,则执行的操作不同,以让单链表继续保持循环的性质。正因为循环单链表是一个“环”,因此在任何一个位置上的插入和删除操作都是等价的,无须判断是否是表尾。

      在单链表中只能从表头结点开始往后顺序遍历整个链表,而循环单链表可以从表中的任意一个结点开始遍历整个链表。有时对单链表常做的操作是在表头和表尾进行的,此时对循环单链表不设头指针而仅设尾指针,从而使得操作效率更高。其原因是,若设的是头指针,对表尾进行操作需要O(n)的时间复杂度,而若设的是尾指针 r,r->next 即为头指针,对于表头和表尾进行操作都只需要O(1)的时间复杂度。

    循环双链表

      由循环单链表的定义推出循环双链表,不同的是在循环双链表中,头结点的prior 指针还要指向表尾结点。

      在循环双链表L中,某结点*p 为尾结点时,p->next == L; 当循环单链表为空表时,其头结点的 prior 和 next 都等于 L。

    静态链表

      静态链表是借助数组来描述线性表的链式存储结构,结点也有数据域data 和指针域 next,与前面讲的链表中的指针不同的是,这里的指针是结点的相对地址(数组下标),又称游标。

    和顺序表一样,静态链表也要预先分配一块连续的内存空间。

      静态链表的结构类型描述如下:

    1 #define MaxSize 50        //静态链表的最大长度
    2 typedef struct {        //静态链表结构类型的定义
    3     ElemType data;        //存储数据元素
    4     int next;            //下一个元素的数组的下标
    5 } SLinkList[MaxSize];

    静态链表以 next ==  -1;作为其结束的标志。静态链表的插入、删除操作与动态链表的相同,只需要修改指针,而不需要移动指针,而不需要移动元素。总体来说,静态链表没有单链表使用起来方便。

    顺序表和链表的比较

    1.存取方式

      顺序表可以顺序存取,也可以随机存取,链表只能从表头存取元素。

    2.逻辑结构与物理结构

      采用顺序存储时,逻辑上相邻的元素,其对应的物理存储位置也是相邻的。而采取链式存储时,逻辑上相邻的元素,其物理存储位置则不一定相邻,其对相应的逻辑关系是通过指针链接来表示的。

      注意区别存取方式和存储方式

    3.查找、插入和删除

      对于按值查找,顺序表无序时,两者的时间复杂度为O(n);顺序表有序时,可采用折半查找,此时的时间复杂度为O(log2n)。

      对于按序号查找,顺序表支持随机访问(即根据数组的下标访问即可),时间复杂度为O(1),而链表的平均时间复杂度为O(n)。顺序表的插入、删除操作,平均需要移动半个表长的元素。链表的插入、删除操作,只需要修改相关结点的指针域即可。由于链表的每个结点都带有指针域,因而在存储空间上要比顺序存储付出的代价大,而存储密度的不够大。

    4.空间分配

      顺序表在静态存储分配的情形下,一旦存储空间装满就不能扩充,若再加入新元素,则会出现内存溢出,因此需要预先分配足够大的存储空间。预先分配过大,可能会导致顺序表后部大量闲置,造成资源浪费;预先分配内存过小,又会造成溢出。动态存储分配虽然存储空间可以扩充,但需要移动大量的元素,导致操作效率降低。而且若内存中没有更大块的连续存储空间,则会导致分配失败。链式存储的结点空间只在需要时申请分配,只要内存中有空间就可以分配,操作灵活、高效。

  • 相关阅读:
    防抖与节流
    两台电脑互联
    es6标签模板转义html
    vue[mini-css-extract-plugin]Conflicting order between 警告解决方式(转载)
    如何解决Windows10处于通知模式(转载)
    hexo与github page搭建博客
    缓动类型参考
    微信代扣-免密支付 开通教程
    Linux服务器安全配置
    在linux (centos)上使用puppeteer实现网页截图
  • 原文地址:https://www.cnblogs.com/ouyang_wsgwz/p/10802520.html
Copyright © 2020-2023  润新知