计算机基础数据结构讲解第七篇-链表操作

  本篇文章我们学习线性表的链式表示,也就是链表。我们知道,顺序表可以随机存取,查找方便,但是插入和删除需要移动大量元素。链式存储线性表的时候,不需要使用地址连续的存储单元,而是通过"链"建立起数据元素之间的逻辑关系,不要求物理位置连续,插入和删除只需要修改指针,很方便。但是这样的话由于不要求物理位置连续,就会失去随机存取的优点。下面就来介绍各种链表的。
  由于链表在网上的介绍很多,所以具体结构就不用图片展示了,直接介绍如何用代码实现。

一:单链表定义

  线性表最简单的链式储存是单链表,是通过一组任意的存储单元存储线性表中的数据元素。和顺序表不同的是,任意代表存储单元位置不相邻,而单链表的结点也和顺序表不同。
  单链表除存放元素自身的信息之外,还需要存放一个指向其后继结点的指针。也就是结点包括数据域和指针域,指针存放继结点的地址,因为附加了一个指针域,所以存在浪费空间,空间利用率不高的情况。因为不是随机存储的,所以就造成了它的缺点是如果需要查找某个特定的结点时,需要从表头开始遍历,依次查找。
  单链表的结点类型描述如下:

typedef struct LNode{
  ElemType data;
  struct LNode *next;
}LNode,*LinkList;

  通常用头指针来标识一个单链表,头指针为NULL表示一个空表。有时候为了操作上的方便,在单链表头指针和第一个结点之间附加一个结点,称为头结点。头结点的数据域可以不包含任何信息,也可以记录表长等信息。头结点的指针域指向线性表的第一个元素结点。
  引入头结点优点:
  在链表的第一个位置上的操作和在表的其他位置上的操作一致,无须进行特殊处理。
  无论链表是否为空,头指针都是指向头结点的非空指针(空表中头结点的指针域为空),空表和非空表的处理得到了统一。

二:单链表的操作

1.头插法建立单链表

  该方法将新结点插入到链表的表头,即头结点之后。
  该算法相当于改变了头结点和其后继结点之间的对应关系。代码如下:

LinkList List_HeadInsert(LinkList &L){
  LNode *s,int x;
  L = (LinkList)malloc(sizeof(LNode));  //创建头结点
  L->next = NULL;                       //初始为空链表
  scanf("%d", &x);
  while(x!=99999){
    //创建新结点,区别头结点用 LNode*指向
    s = (LNode*)malloc(sizeof(LNode));
    s->data = x;
    s->next = L->next;
    L->next = s;
    scanf("%d", &x);
  }
  return L;
}

  由于头插法是在表头插,只改变头结点的指向,则插入后的时间复杂度为O(1),则对于一个表长为n的单链表,头插法建立完单链表的总的时间复杂度为O(n)。

2.尾插法建立单链表

  还有一种算法是在链表的表尾插入,因此必须增加一个尾指针r,使其指向当前链表的尾结点,然后把尾结点的指针指向要插入的元素。代码如下:

LinkList List_TailInsert(LinkList &L){
  int x;
  L = (LinkList)malloc(sizeof(LNode));    //创建头结点
  LNode *s,*r = L;                        //r为尾指针
  scanf("%d", &x);
  while(x!=99999){
    s = (LNode*)malloc(sizeof(LNode));
    s->data = x;
    r->next = s;
    r=s;                                  //r指向新的尾结点
    scanf("%d", &x);
  }
  s->next = NULL;                         //尾指针置为空
  return L;
}

  因为附设了一个指向尾结点的指针,所以时间复杂度和头插法相同。

3.按序号查找结点值

  这个就是最简单查找结点的方法,从第一个结点出发,直到找到第i个结点为止,否则返回最后一个节点的指针域NULL。代码如下:

LNode *GetElem(LinkList L,int i){
  int j = 1;                          //计数,初始值为1
  LNode *p = L->next;                 //头结点赋值给p
  if(i == 0)
    return L;                         //若i等于0,则返回头结点
  if(i < 1)
    return NULL;                      //若i无效,则返回NULL
  //从第1个结点开始找,知道找到第i个结点
  while(p && j < i){
    p = p->next;
    j++;
  }
  //返回第i个结点的指针,若i大于表长则返回NULL
  return p;
}

4.按值查找结点值

  这个也是最简单查找结点的方法,从第一个结点出发,直到找到第1个结点等于e的值为止,否则返回最后一个节点的指针域NULL。代码如下:

LNode *LocateElem(LinkList L,int e){
  LNode *p = L->next;
  //从第一个结点查找data域为e的结点
  while(p != NULL && p->data != e)
    p = p->next;
  //找到后返回该结点指针,否则返回NULL
  return p;
}

  因为最快情况要遍历整个链表,所以按值查找和按位查找元素的平均时间复杂度都是O(n)。

5.随机插入结点操作

  该操作把值为x的新结点插入到单链表的第i个位置上。首先调用查找算法GetElem(L,i-1),查找到第i-1个结点,然后改变指针即可。算法的位置不能颠倒,否则会造成等号两边不相等的错误,代码如下:

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

  本算法的时间主要花在查找第i-1个元素,时间复杂度是O(n),如果给定结点插入新结点,时间复杂度仅为O(1)。该算法是前插操作。
  也可以对线性表进行前插操作,但是也可以转化为后插操作,重要的是找到前插位置元素的前驱结点。时间复杂度为O(n)。也可以用如下的算法转化为后插操作实现,代码如下:

//和之前的后插操作一样
s->next = p->next;
p->next = s;
temp = s->data;
s->data = p->data;
p->data = temp;
//交换数据域
6.删除结点操作

  删除结点操作将单链表的第i个结点删除先找到要删除元素的前驱结点,然后修改指针,就可以将其删除。代码如下:

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

  由于删除操作也是先要找到其前驱结点,因此算法的主要时间也耗费在查找操作上,时间复杂度为O(n)。
  也可以用其他方法删除结点。

7.求表长操作

  求表长的操作就是计算单链表中数据结点的个数,需要设置一个计数器变量,最后求得表长,算法的时间复杂度为O(n)。

8.注意事项

  由于单链表的长度是不包括头结点的,因此不带头结点和带头结点的单链表在求表长操作上会有不同。

三:双链表

  单链表只有一个指针,因此单链表只能依次从后往前遍历,访问后继结点的时间复杂度是O(1),访问前驱结点的时间复杂度是o(n)。
  为了克服这种缺点,引入了双链表,有两个指针prior和next。结构代码如下:

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

  双链表增加了一个指向前驱的prior指针,因此按值查找和安危查找的操作与单链表相同。但双链表在插入和删除操作的实现上,与单链表有着较大的不同。是由于也要对prior指针修改,保证在修改过程中不断链。双链表很方便地找到其前驱结点,因此,插入和删除操作的时间复杂度仅为O(n)。

1.双链表的插入操作

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

2.双链表的删除操作

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

四.其他链表

  还有其他链表,这里进行介绍.

1.循环单链表

  循环单链表相当于把最后一个结点的指针从NULL改为指向头结点,从而使整个链表形成一个环。因此循环单链表的判空条件是指针是否等于头指针。
  循环单链表插入,删除算法和单链表一样,不用的是如果操作在表尾进行,让单链表保持循环的性质。正是因为循环单链表是一个环,所以在任何位置上的插入和删除操作都是等价的,无需判断是否是表尾。
  循环单链表可以从表中的任意一个结点开始遍历单链表。有时操作在表头和表尾进行,仅设尾指针,使操作效率更高。是由于r->next即为头指针,对于表头和表尾的操作都只需要O(1)的时间复杂度。

2.循环双链表

  循环双链表就是循环单链表然后加上头结点的prior指针还要指向表尾结点。在循环双链表中,某结点为尾结点时,它的指针域是头指针;当循环双链表为空表时,头结点的prior和next域都等于头指针。

3.静态链表

  静态链表借助数组表示,也有数据域和指针域,但这里的指针是结点的相对地址,也就是数组下标,又称游标,需要预先分配一块连续的内存空间。可以理解为顺序表数据元素加一个指针域。
  静态链表以next==-1作为结束的标志。静态链表的插入,删除操作与动态链表相同,但没有单链表使用方便。是为了不支持指针的高级语言设计的,比如python,Basic。

posted @ 2020-09-02 20:07  一只帅气的IT小昂  阅读(1106)  评论(0编辑  收藏  举报