数据结构之链表

Posted 玄鸟轩墨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构之链表相关的知识,希望对你有一定的参考价值。

写在前面

谈完了顺序表,这就到了链表了。在这里我们不得不提一下链表的优点。没办法,优秀的事物总是比较出来的。今天的任务不简单。我们要学习知识很多。本且这也是我们在找工作的时候必考常考的知识点。

链表

在《漫画算法:小灰的算法之旅》一书中,举了一个很有趣的例子,它把顺序表比作正规军,链表就是地下党,这是很形象的,顺序表的物理结构是一个数组,我们是顺序存储,但是链表是随机存储的,我们有的时候只能找到它的上级和下级,不能跳跃寻找,这就像地下工作人员一样。

由于数据结构这个模块大部分使用C语言来写的,要求我们学习这个我们需要对结构体、指针以及内存管理有充分的了解。所以说我们的C语言知识基础一定要扎实.

什么是链表

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中指针链接次序实现的 。

下面的是一个结构体

typedef int SLDataType;     // 数据类型

typedef struct SListNode

    SLDataType val;
    struct SListNode* next;  //记录下一个节点的地址
SListNode;

我们来看看一个完整的单向链表,下面就是我们要学习的知识点

为何存在链表

我们很疑惑,既然存在来了顺序表。我们为什么还要链表呢。这是由于顺序表存在一定的缺陷

  1. 中间/头部的插入删除,时间复杂度为O(N)
  2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
  3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们
    再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间

我们看看增容的缺陷 当我们使用realloc来进行增容时,要是后面增加的空间太大,会出现==异地扩容==,这就出现了不必要的消耗

void test()

    int* p1 = (int*)malloc(40);
    printf("%p\\n", p1);
    p1 = (int*)realloc(p1, 100000);
    printf("%p\\n", p1);
    free(p1);

int main()

    test();
    return 0;

链表分类

链表也是有一定分类的,不过本质都一样,组合起来分为八种

  • 是否带头
  • 单向还是双向
  • 是否循环

单链表

我们先说一下单向不带头的链表,先学会基础的后面大家看起来会比较容易。

打印

为了测试代码的正确性,我们先看看如何打印单链表,后面要辅助我们验证代码的正确性

void SListPrint(SListNode* pHead)

    SListNode* cur = pHead;
    while (cur != NULL)
    
        printf("%d ->", cur->val);
        cur = cur->next;
    
    printf("NULL\\n");

插入数据

一般而言,单链表数据的插入分为头插和尾插两种,我们一般很少使用其他的插入方式,不是说这些方式不能实现,而是没有太大的价值.

头插

  1. 链表是一个空链表,插入的节点就是头节点
  2. 链表是一个正常的,插入一个节点.

  • 为何传入的是二级指针? 要是我们第一次头插,需要改变头节点的地址,传入一级指针(头节点的地址),改变的是形参
  • 为何定义BuySListNode(x);,我们发现,后面的尾插也需要创造一个节点,避免代码的重复造轮子,我们单独拿出来
SListNode* BuySListNode(SLDataType x)

    SListNode* node = (SListNode*)malloc(sizeof(SListNode));
    if (node == NULL)
    
        printf("malloc fail!\\n");
        exit(-1);
    
    node->next = NULL;
    node->val = x;
    return node;


void SListPushFront(SListNode** pplist, SLDataType x)

    assert(pplist);
    SListNode* node = BuySListNode(x);
    //判断是不是头一次插入
    if (*pplist == NULL)
    
        *pplist = node;
        return;
    
    node->next = *pplist;
    *pplist = node;
int main()

    SListNode* pHead = NULL;

    SListPrint(pHead);
    SListPushFront(&pHead, 3);
    SListPushFront(&pHead, 2);
    SListPushFront(&pHead, 1);
    SListPrint(pHead);
    return 0;

尾插

头插说完了,我们要说说尾插,一般而言,尾插要比头插应用的更多一点,我们一般都是在尾部插入数据,这样更符合我们的想法.

void SListPushBack(SListNode** ppHead, SLDataType x)

    assert(ppHead);
    SListNode* node = BuySListNode(x);
    // 第一次插入 
    if (*ppHead == NULL)
    
        *ppHead = node;
        return;
    
    SListNode* cur = *ppHead;
    while (cur->next != NULL)
    
        cur = cur->next;
    

    cur->next = node;

删除数据

插入数据已经说完了,现在我们看看如何删除数据.

头删

头删就是删除第一个节点,在顺序表中,我们可能要移动很多的数据,但是单链表不需要,你会发现它的优点

void SListPopFront(SListNode** ppHead)

    assert(ppHead);

    if (*ppHead == NULL)
    
        printf("没有节点");
        return;
    
    SListNode* cur = (*ppHead)->next;
    free(*ppHead);
    *ppHead = cur;
int main()


    SListNode* pHead = NULL;

    SListPushBack(&pHead, 1);
    SListPushBack(&pHead, 2);
    SListPushBack(&pHead, 3);
    SListPrint(pHead);

    SListPopFront(&pHead);

    SListPrint(pHead);

    SListDestory(&pHead);
    return 0;

尾删

链表的尾删和尾插一样,都需要找到最后一个节点,不过尾删需要把倒数第二个节点的next置为NULL,这是一定需要的做的.

void SListPopBack(SListNode** ppHead)

    assert(ppHead);
    if (*ppHead == NULL)
    
        printf("没有节点");
        return;
    
    SListNode* cur = *ppHead;

    // 只有 一个节点
    if (cur->next == NULL)
    
        free(cur);
        *ppHead = NULL;
        return;
    
    SListNode* curNext = cur->next;

    while (curNext->next != NULL)
    
        cur = curNext;
        curNext = curNext->next;
    
    free(curNext);
    cur->next = NULL;
int main()


    SListNode* pHead = NULL;

    SListPushBack(&pHead, 1);
    SListPushBack(&pHead, 2);
    SListPushBack(&pHead, 3);
    SListPrint(pHead);
    SListPopBack(&pHead);
    SListPrint(pHead);

    SListDestory(&pHead);
    return 0;

查找数据

我们可以在单链表中查看val的值是不是一样的,一样的话就返回这个节点的地址,不是就返回NULL,数据的查找在现实生活中很大的作用

SListNode* SListFind(SListNode* pHead, SLDataType key)

    assert(pHead);
    SListNode* cur = pHead;

    while (cur != NULL)
    
        if (cur->val == key)
        
            return cur;
        
        cur = cur->next;
    
    return NULL;
int main()

    SListNode* pHead = NULL;
    SListPushBack(&pHead, 1);
    SListPushBack(&pHead, 2);
    SListPushBack(&pHead, 3);
    SListNode* ret = SListFind(pHead, 2);
    if (ret != NULL)
    
        printf("%d", ret->val);
    
    SListDestory(&pHead);
    return 0;

单链表在pos位置之后插入x

前面我们就谈了查找数据时就返回了节点的地址,做的铺垫就是为了这个.不过我们要考虑一下,为何不在pos前面插入数据呢?很简单,我们使用的是单链表,得到后面的节点的地址很容易,但是想要得到前面的地址,你就慢慢玩吧.

void SListInsertAfter(SListNode* pos, SLDataType x)

    assert(pos);
    SListNode* cur = pos->next;
    SListNode* node = BuySListNode(x);
    pos->next = node;
    node->next = cur;
int main()


    SListNode* pHead = NULL;

    SListPushBack(&pHead, 1);
    SListPushBack(&pHead, 2);
    SListPushBack(&pHead, 3);
    SListPrint(pHead);
    SListNode* ret = SListFind(pHead, 2);

    SListInsertAfter(ret,0);
    SListPrint(pHead);

    SListDestory(&pHead);
    return 0;

单链表删除pos位置之后的值

我们刚才说了pos后面插入,现在应该说说删除,这些都是很简单的

void SListEraseAfter(SListNode* pos)

    assert(pos);
    SListNode* cur = pos->next;
    //判断  pos 是不是 尾节点
    if (cur == NULL)
    
        return;
    
    pos->next = cur->next;
    free(cur);
int main()


    SListNode* pHead = NULL;

    SListPushBack(&pHead, 1);
    SListPushBack(&pHead, 2);
    SListPushBack(&pHead, 3);

    SListPrint(pHead);
    SListNode* ret = SListFind(pHead, 1);
    SListEraseAfter(ret);
    SListPrint(pHead);
    SListDestory(&pHead);
    return 0;

销毁链表

有的人可能会疑惑,我们为何要最后销毁链表呢?我们节点的空间都是malloc出来的,使用完比后我们要释放,不然会出现内存泄漏.或许你觉得这点内存的没啥,但是要是我们链表过长,次数过多,服务器的内存会一点一点变小.最后你就被领导骂了.

void SListDestory(SListNode** ppHead)

    assert(ppHead);
    SListNode* cur = *ppHead;
    while (cur != NULL)
    
        SListNode* curNext = cur->next;
        free(cur);
        cur = curNext;
    
    *ppHead = NULL;

双向带头循环链表

单链表我们就已经说的很详细了,不过单链表还是有一定的缺陷,

  • 不能得到前面节点的地址
  • 尾插的时候我们需要遍历整个链表,时间复杂度O(N).

这些都是问题,我,我们想一想是不是有个链表来解决这些问题.很高兴双向循环链表可以解决这些问题.我们这里的方法和上面的几乎是一样的,这里就不加动图了.不过我会用文字详细的描述出来.

节点

typedef int LTDataType;
typedef struct ListNode

    LTDataType data;
    struct ListNode* next;
    struct ListNode* prev;
ListNode;

创造头节点

上面的单链表我们没有添加标兵位,这里添加一下吧.

ListNode* ListCreate()

    ListNode* cur = (ListNode*)malloc(sizeof(ListNode));
    assert(cur);
    cur->data = -1;
    cur->next = cur;
    cur->prev = cur;
    return cur;
int main()

    ListNode* paceSetter = ListCreate(); //标兵

    return 0;

打印

和前面一样,我们需要测试代码的正确性,这里先给出打印的函数。

  • 创造一个节点,指向标兵位的next
  • 打印这个数值,这个节点一直往后移,直到节点的地址等于标兵位
void ListPrint(ListNode* plist)

    assert(plist);
    ListNode* cur = plist->next;
    while (cur != plist)
    
        printf("%d->", cur->data);
        cur = cur->next;
    
    printf("NULL\\n");

插入元素

头插

需要完成一下步骤,

  1. 创造一个节点
  2. 新节点的next保存原来的头节点
  3. 原来的头节点的prev保存新节点
  4. 标兵位next保存新节点的位置,新节点的prev保存标兵位
void ListPushFront(ListNode* plist, LTDataType x)

    ListNode* node = (ListNode*)malloc(sizeof(ListNode));
    assert(node);
    node->data = x;

    node->next = plist->next;
    node->next->prev = node;

    plist->next = node;
    node->prev = plist;

尾插

尾插更简单,使用单链表的时候还需要一步一步寻找尾节点,时间复杂度O(N)这里不需要,我们可以观察到标兵位的prev就是尾节点的地址。

  • 创造一个节点
  • 拿一个节点指向尾节点
  • 依次修改尾节点和标兵位相应的值
void ListPushBack(ListNode* plist, LTDataType x)

    ListNode* node = (ListNode*)malloc(sizeof(ListNode));
    assert(node);
    node->data = x;
    ListNode* tail = plist->prev;
    //尾节点的next指向 node
    tail->next = node;

    //node的prev指向 tail
    node->prev = tail;

    //node的next指向标兵位
    node->next = plist;

    //标兵位的prev指向node
    plist->prev = node;

删除元素

头删

记住头删不是删除标兵位,是有效节点。

  • 拿个节点记录头节点,判断头节点是不是和标兵位相同
  • 记录头节点的下一个节点,依次修改相应的值。
void ListPopFront(ListNode* plist)

    assert(plist);
    ListNode* head = plist->next;
    if (head == plist)
    
        return;
    
    ListNode* headNext = head->next;
    head->next = NULL;
    head->prev = NULL;
    free(head);
    plist->next = headNext;
    headNext->prev = plist;

尾删

尾删很简单,我们找到最后的尾节点,一步修改相应的next和prev就可以了在,这里就不和大家分享了。

void ListPopBack(ListNode* plist)

    assert(plist);
    ListNode* tail = plist->prev;
    if (tail == plist)
    
        return;
    
    tail->prev->next = plist;
    plist->prev = tail->prev;
    tail->next = NULL;
    tail->prev = NULL;
    free(tail);

链表总结

到这里,大家应该对链表的理论有一定的了解了吧,不过这是远远不够的,我们需要一定的习题来练习,锻炼我们的思维。这里我把一些简单的OJ题给大家列出来吧,里面只有答案,没有讲解,不过这些都是比较简单的,大家坐一坐,提高一下能力。

简单链表习题集合

以上是关于数据结构之链表的主要内容,如果未能解决你的问题,请参考以下文章

数据结构实验之链表三:链表的逆置

数据结构实验之链表二:逆序建立链表

数据结构之链表

数据结构之链表

数据结构之链表(LinkedList)

数据结构之链表