链表(linked list)

来源:互联网 发布:基于java的毕业论文 编辑:程序博客网 时间:2024/06/05 07:18

链表的特点

  • 根据线性表元素多少,长度可变化。
  • 动态申请内存空间:元素删除或插入时,对应申请新的存储空间或释放原来占有的存储空间。
  • 使用指针来链接各个结点,按照线性表的前驱关系将结点连接起来,结点之间内存地址不必相邻。
  • 链表的访问:不能像数组那样根据下标直接访问某一个结点i,而需要从头结点开始,沿着link指针一个一个地计数(遍历),才能找到。
  • 通常有一个first指针变量和一个last指针遍历,用来存储头结点地址和末尾结点的地址(便于快速在链表末尾添加结点)。

结点数据结构

单链表结点的数据结构通常分为两部分:数据和指向后继结点的指针。

struct ListNode {    ELEM data;    ListNode * link;};ListNode *first, *last;

空链表的表示:

为了表示空链表,引入一个特殊的“首结点”,该节点中data的值被忽略,该节点不被看做列表中的实际元素,只是为了方便空链表操作和表头操作。当链表为空时,first和last均指向首结点,如下:

ListNode * headNode = new ListNode;first =  headNode;last = headNode;

相关算法

查找结点

查找位置为i的结点。

ListNode * find(const int i) {    // 返回“首结点”,    if (-1 == i) {        return first;    }    // 指向第一个结点;    ListNode * p = first->link;    int j=0;    // 遍历到链表末尾或j==i时停止。    while(p != NULL && j<i) {        p = p->link;        j++;    }    // 当链表结点数量小于i时,返回NULL    return p; }

新增结点

关键点:

  • 找到前序结点,注意前序结点为NULL的情况;
  • 先将新增结点的data和link设置好,新增结点的link指向前序结点的link;
    再将其前序结点的link指向新增结点。
  • 注意要考虑到新增结点在链首、中、尾这三种情况的处理,插入到链表末尾时,last指针指向该新增结点。
ListNode * insert(ELEM value, int i) {    ListNode * preNode;    // 查找到前序结点,因为特殊“首结点”的存在,使i==0的情况不用特殊处理。    preNode = find(i - 1);    if (NULL == preNode) {        return NULL;    }    ListNode * newNode;    newNode = new ListNode;    newNode->link = preNode->link;    newNode->data = value;    preNode->link = newNode;    // 处理插入到链表尾部的情况    if (NULL == newNode->link) {        last = newNode;    }    return newNode;}

删除结点

删除某一个结点的后续结点。
关键点:

  • 在真正删除节点之前,安排好它的“后事”:将其前序结点的link指向待删除结点的link;
void removeAfter(ListNode * preNode) {    // 暂存待删除结点    ListNode * delLink = preNode->link;    if (delLink != NULL) {        preNode->link = delLink->link;        delete delLink;    }}

链表长度

int length() {    ListNode * curNode = first->link;    int count = 0;    while (curNode != NULL) {        curNode = curNode->link;        count++;    }    return count;}

单链表的缺点

  • 可以方便地查询某个结点的后续结点,但不能直接查询其前驱结点。

顾名思义,它是双向列表。主要在其每个结点中增加了一个指向前驱的指针rlink,指向后续结点的指针为llink;

结点结构

struct DblListNode {    ELEM data;    DblListNode * preLink;    DblListNode * nextLink;};DblListNode *first, *last;

空链表表示和单链表一样,使用一个特殊“首结点”,其preLink为NULL。

相关算法

新增结点

在某个结点的后面新增一个结点。

关键点:

  • 修改旧有结点的preLink或nextLink之前,一定要确定它的旧值是否已经利用完了?
DblListNode * insertAfter(ELEM value, dblListNode * node) {    DblListNode * preNode = node->preLink;    DblListNode * nextNode = node->nextLink;    // 设置新增结点的各字段    DblListNode * newNode = new DblListNode;    newNode->data = value;    newNode->nextLink = node->nextLink;    newNode->preLink = node;    node->llink->rlink = newNode;    node->llink = newNode;    return newNode;}

删除结点

删除某个指定的结点。
关键点:

  • 删除前处理好“后事”:如待删除结点的前驱和后续结点是否已安排妥当?
  • 待删除结点的preLink和nextLink置为空。
void deleteNode(DblListNode * node) {    node->preLink->nextLink = node->nextLink;    node->nextLink->preLink = node->preLink;    node->preLink == NULL;    node->nextLink == NULL;    delete node;}

链表 vs 数组

数组

数组的优点

  • 没有使用指针,节省存储空间(因为指针需要2或4字节存储);
  • 可直接访问指定下标的元素,简洁便利,程序更易懂;

数组的缺点

  • 不可动态改变长度,必须事先确定数组长度。
  • 往数组中插入、删除某个元素时,可能需要移动O(k)个元素。
  • 需要连续的一大块存储空间,以存储所有的元素。

链表

链表的优点

  • 无需事先确定长度,可事先动态增、删元素。
  • 删除、插入元素时不需要移动O(k)个元素。
  • 结点间的存储空间不需要连续,因而可以利用一些碎片空间。

应用场合

  • 当线性表中需要经常删除、插入元素时,不使用数组,使用链表。
  • 当线性表长度不确定时,不能使用数组,使用链表,否则可能为了预留足够的空间而定义一个很大的数组,造成资源浪费。

  • 当读取操作频繁(特别是按位读取操作频繁),插入、删除操作不频繁的时候,可以使用数组。

  • 当存储的数据占的存储空间与指针占有的空间相当时(1:1),要慎重考虑使用链表是否值得。
  • 当元素内容经常更新,但删除、插入操作并不常见的时候,可以考虑使用数组作为一个“索引”,即数组中元素为一个指针,指针指向内容真正存储的地址,这样就很容易找到内容存储地址,并进行更新。
原创粉丝点击