链表(linked list)

链表的特点

  • 根据线性表元素多少,长度可变化。
  • 动态申请内存空间:元素删除或插入时,对应申请新的存储空间或释放原来占有的存储空间。
  • 使用指针来链接各个结点,按照线性表的前驱关系将结点连接起来,结点之间内存地址不必相邻。
  • 链表的访问:不能像数组那样根据下标直接访问某一个结点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),要慎重考虑使用链表是否值得。
  • 当元素内容经常更新,但删除、插入操作并不常见的时候,可以考虑使用数组作为一个“索引”,即数组中元素为一个指针,指针指向内容真正存储的地址,这样就很容易找到内容存储地址,并进行更新。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值