40.单向链表和双向链表


40.1.从数组的缺陷说起
(1)数组有2个缺陷,1个是数组中所有元素的类型必须相同;第2个是数组的元素个数必须事先规定并且指定之后不能更改;数组的第1个缺陷靠结构体去解决(结构体允许其中的元素的类型不相同);数组的第2个缺陷可通过对数组进行封装/使用链表解决(链表的大小可实时扩展)。
(2)变量定义并初始化后,操作系统给该变量分配了相应的内存空间,该变量分配了内存空间之后,该变量内存的相邻区域又分配了其他变量与这个变量地址相连,出于业务需要,改变量需要更大的内存空间,则有拆迁/搬迁/外部扩展这3种方法。
(3)拆迁基本行不通,成本太高了;搬迁可以行的通,程序中解决数组大小扩展的1个思路就是整体搬迁,即先在另外的空白内存处建立1个大的数组,然后把原来的数组中的元素的值整个复制到新数组的头部,然后再释放掉原来数组的内存空间,该可变数组在C语言中不支持,但在C++/Java中支持;外部扩展的思路是最合理的,它的核心思想是化整为零,在原来的内存空间不改变的前提下去外部扩展新的分基地(即使用链表)。


40.2.链表是什么样的
(1)链表即用锁链把每个表链接起来构成的数据结构;该处的表指的是节点(节点中的内存可以用来存储数据即称为数据表),该处的锁链指的是链接各个表的方法(C语言中用来连接2个表的方法即指针)。
(2)链表是由若干个节点组成的(链表的各个节点结构是完全相同);节点由有效数据和指针组成;有效数据区域用来存储信息;指针区域用于指向链表的下1个节点。
(3)链表就是用来解决数组的大小不能动态扩展的问题,则链表其实就是当数组用的;即链表能完成的任务用数组也能完成,数组能完成的任务用链表也能完成,但是灵活性不一样。
(4)链表的本质目的是用来存储数据的;链表存储数据相对于数组来说的优点就是灵活性,需要多少个内存空间就动态分配多少个,不占用额外的内存;数组的优势是使用简单(简单粗暴)。


40.3.单链表的实现
(1)单链表的节点=有效数据+指针;定义的struct_node只是构造了1个结构体类型,本身并没有变量生成,也不占用内存;结构体定义相当于为链表节点定义了1个模板,后续在实际创建链表时需要节点时就用该模板来复制1个即可。
(2)链表的内存要求比较灵活,不能用栈,也不能用data数据段,只能用堆内存;使用堆内存来创建1个链表节点的步骤=申请堆内存,大小为一个节点的大小(检查申请结果是否正确);清理申请到的堆内存;把申请到的堆内存当作1个新节点;填充你哦个新节点的有效数据和指针区域。
(3)链表的头指针并不是节点,而是1个普通指针,只占4字节;头指针的类型是struct_node_*类型。
(4)典型的链表的实现=头指针指向链表的第1个节点;第1个节点中的指针指向下1个节点;依次类推直到最后的节点。


40.4.单链表的算法之插入节点
(1)访问链表中各个节点的数据只能用头指针,不能用各个节点自己的指针,因为我们保存链表的时候是不会保存各个节点的指针的,我们只能通过头指针来访问链表节点;前1个节点内部的pNext指针能帮助我们找到下1个节点。
(2)链表带头节点的用法即把头指针指向的第1个节点作为头节点使用;首先头节点紧跟在头指针后面;其次头节点的数据部分是空的(有时候存储整个链表的节点数);然后头节点的指针部分指向下1个节点,即第1个节点;头节点在创建头指针时一并创建并且和头指针关联起来,后面的真正的存储数据的节点用节点添加函数来完成。
(3)链表有没有头节点在链表的插入节点+删除节点+遍历节点+解析链表的各个算法函数的体现上不同;所以若链表设计的时候就有头节点那么后面的所有算法都应该这样来处理;如果设计时就没有头节点那么后面的所有算法都应该按照没有头节点来做;实际编程中两种链表都有人用。
(4)链表可以从头部插入,也可以从尾部插入,也可以两头分别插入;头部插入和尾部插入对链表本身无差别,但有时候对业务逻辑有差别。


40.5.单链表的算法之遍历节点
(1)遍历就是把单链表中的各个节点挨个拿出来,就叫遍历;遍历的要点=不能遗漏+不能重复+追求效率;分析某个数据结构如何遍历,关键是分析该数据结构本身的特点,然后根据本身特点来制定它的遍历算法。
(2)单链表的特点就是由很多个节点组成;头指针+头节点为整个链表的起始;最后1个节点的特征是它内部的pNext指针值为NULL;从起始到结尾中间由各个节点内部的pNext指针来挂接;由起始到结尾的路径有且只有1条。
(3)单链表到遍历方法是从头指针+头节点开始,顺着链表挂接指针依次访问链表的各个节点,取出这个节点的数据,然后再往下1个节点,直到最后1个节点,结束返回。


40.6.单链表的算法之删除节点
(1)删除节点首先需找到要删除的节点,然后删除这个节点;通过遍历来查找节点,从头指针+头节点开始,顺着链表依次将各个节点拿出来,按照特定的方法比对,找到我们要删除的那个节点;待删除的节点不是尾节点的情况,首先把待删除的节点的前1个节点的pNext指针指向待删除的节点的后1个节点的首地址,然后再将该摘出来的节点free掉;待删除的节点是尾节点的情况,首先把待删除的尾节点的前1个节点的pNext指针指向null,然后将摘出来的节点free掉。


40.7.单链表的算法之逆序
(1)链表的逆序就是把链表中所有的有效节点在链表中的顺序给反过来;操作数据结构的算法的第1个层次是数学和逻辑上的算法;第2个层次是用编程语言来实现算法。
(2)从逻辑上来讲,链表的逆序有很多种方法,这些方法都能实现最终的需要,但是效率是不同,彼此的可扩展性和容错性等不同;首先遍历原链表,然后将原链表中的头指针和头节点作为新链表的头指针和头节点,原链表中的有效节点挨个依次取出来,采用头插入的方法插入新链表中即可(链表逆序=遍历+头插入);也可使用冒泡法进行逆序。


40.8.双链表的引入和基本实现
(1)单链表解决了数组的大小不能实时扩展的问题;单向链表使用堆内存来存储数据,将数据分散到各节点中,其各个节点在内存中可以不相连,节点之间通过指针进行单向链接;链表中的各个节点内存不相连,有利于利用碎片化的内存。
(2)单链表各个节点之间只由单个指针单向链接,则其局限性主要体现在单链表的单向移动性导致我们在操作单链表时,当前节点只能向后移动不能向前移动,则不利于解决更复杂的算法。
(3)解决思路是使用双向链表;单链表的节点=有效数据+指针(指针指向后1个节点);双向链表的节点=有效数据+2个指针(1个指向后1个节点,另1个指向前1个节点)。


40.9.双链表的算法之遍历节点
(1)双链表是单链表的1个父集,双链表中若完全无视pPrev指针,则双链表就变成了单链表;则双链表的正向遍历(后向遍历)和单链表是完全相同的。
(2)双链表中因为多了pPrev指针,因此双链表还可以前向遍历(从链表的尾节点向前面依次遍历直到头节点);但前向遍历的意义并不大,主要是因为很少出现当前到了尾节点需要前向遍历的情况。
(3)双链表是对单链表的1种有成本的扩展,但是该扩展在有些时候意义不大,在某些时候意义就比较大;因此在实践用途中要根据业务要求选择适合的链表。


40.singly_linked_list
/*
 * 公司:XXXX
 * 作者:Rston
 * 博客:http://blog.csdn.net/rston
 * GitHub:https://github.com/rston
 * 项目:单向链表和双向链表
 * 功能:演示单向链表的各种基本算法。
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* 构建链表节点类型 */
struct node
{
    int data;               // 有效数据
    struct node *next;      // 指向下个节点的指针
};

/* 创建新的节点 */
struct node *creat_node(int data)
{
    // 申请空间
    struct node *p = (struct node*)malloc(sizeof(struct node));
    if (NULL == p)
    {
        printf("malloc error.\n");
        return NULL;
    }

    // 清理空间
    memset(p, 0, sizeof(struct node));

    // 填充空间
    p->data = data;
    p->next = NULL;
}

/* 从尾部插入节点 */
void insert_tail(struct node *pH, struct node *new)
{
    struct node *p = pH;

    // 找到链表中最后1个节点
    while (NULL != p->next)
    {
        p = p->next;    // 往后走1个节点
    }

    // 在最后1个节点后添加新节点
    p->next = new;

    // 节点个数加1
    pH->data++;
}

/* 从头部插入节点 */
void insert_head(struct node *pH, struct node *new)
{
    // 新节点中的next指向原先的第1个节点
    new->next = pH->next;

    // 头节点的next指向新节点
    pH->next = new;

    // 节点个数加1
    pH->data++;
}

/* 通过头指针遍历节点 */
void traversal(struct node *pH)
{
    struct node *p = pH;

    printf("The cnts of node is %d.\n", pH->data);
    while (NULL != p->next)
    {
        // 往后走1个节点
        p = p->next;
        printf("node data is %d.\n", p->data);
    }
    printf("Traversal Down.\n");
}

/* 根据节点数据找到并删除节点 */
int delete_node(struct node *pH, int data)
{
    struct node *p = pH;        // 指向当前节点
    struct node *pPrev = NULL;  // 指向当前节点的前1个节点

    while (NULL != p->next)
    {
        pPrev = p;               
        p = p->next;            

        if (data == p->data)
        {
            if (NULL == p->next)
            {
                // 找到的节点是尾节点
                pPrev->next = NULL;
            }
            else
            {
                // 找到的节点是普通节点
                pPrev->next = p->next;
            }
            free(p);

            return 0;
        }

        // 节点个数减1
        pH->data--;
    }

    printf("can't find the node.\n");
    return -1;
}

/* 通过冒泡法将链表逆序 */
void reverse_linkedlist(struct node *pH)
{
    struct node *p = pH->next;      // pH指向头节点,p指向第1个有效节点
    struct node *pBack;             // 保存当前节点的后一个节点地址

    // 当链表没有有效节点或者只有一个有效节点时,逆序不用做任何操作
    if ((NULL ==p) || (NULL == p->next))
        return;

    while (NULL != p->next)
    {
        pBack = p->next;
        p->next = pBack->next;
        pBack->next = pH->next;
        pH->next = pBack;
    }
}

int main(int argc, char **argv)
{
    // 创建头指针和头节点并将头指针指向头节点
    struct node *pHeader = creat_node(0);

    // 测试创建节点+头插入节点+遍历节点
    insert_head(pHeader, creat_node(11));
    insert_head(pHeader, creat_node(12));
    insert_head(pHeader, creat_node(13));
    traversal(pHeader);

    // 测试尾插入节点
    insert_tail(pHeader, creat_node(14));
    insert_tail(pHeader, creat_node(15));
    insert_tail(pHeader, creat_node(16));
    traversal(pHeader);

    // 测试删除节点
    delete_node(pHeader, 13);
    delete_node(pHeader, 14);
    traversal(pHeader);

    // 测试逆序链表
    reverse_linkedlist(pHeader);
    traversal(pHeader);

    return 0;
}

40.double_linked_list
/*
 * 公司:XXXX
 * 作者:Rston
 * 博客:http://blog.csdn.net/rston
 * GitHub:https://github.com/rston
 * 项目:单向链表和双向链表
 * 功能:演示双向链表的各种基本算法。
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* 构建链表节点类型 */
struct node
{
    int data;               // 有效数据
    struct node *prev;      // 指向上个节点的指针
    struct node *next;      // 指向下个节点的指针
};

/* 创建新的节点 */
struct node *creat_node(int data)
{
    // 申请空间
    struct node *p = (struct node*)malloc(sizeof(struct node));
    if (NULL == p)
    {
        printf("malloc error.\n");
        return NULL;
    }

    // 清理空间
    memset(p, 0, sizeof(struct node));

    // 填充空间
    p->data = data;
    p->prev = NULL;
    p->next = NULL;
}

/* 从尾部插入节点 */
void insert_tail(struct node *pH, struct node *new)
{
    struct node *p = pH;

    // 找到链表中最后1个节点
    while (NULL != p->next)
    {
        p = p->next;    // 往后走1个节点
    }

    // 在最后1个节点后添加新节点
    new->prev = p; 
    p->next = new;

    // 节点个数加1
    pH->data++;
}

/* 从头部插入节点 */
void insert_head(struct node *pH, struct node *new)
{
    // 新节点的next指针指向原来的第1个有效节点的地址
    new->next = pH->next;

    // 原来第1个有效节点的prev指针指向新节点的地址
    if (NULL != pH->next)
        pH->next->prev = new;

    // 头节点的next指针指向新节点地址
    pH->next = new;

    // 新节点的prev指针指向头节点的地址
    new->prev = pH;

    // 节点个数加1
    pH->data++;
}

/* 通过头指针向后遍历节点 */
void traversal_next(struct node *pH)
{
    struct node *p = pH;

    printf("The cnts of node is %d.\n", pH->data);
    while (NULL != p->next)
    {
        // 往后走1个节点
        p = p->next;
        printf("node data is %d.\n", p->data);
    }
    printf("Traversal Down.\n");
}

/* 通过头指针向前遍历节点 */
void traversal_prev(struct node *pT)
{
    struct node *p = pT;

    while (NULL != p->prev)
    {
        printf("node data is %d.\n", p->data);
        // 往前走1个节点
        p = p->prev;
    }
    printf("Traversal Down.\n");
}

/* 根据节点数据找到并删除节点 */
int delete_node(struct node *pH, int data)
{
    struct node *p = pH;        // 指向当前节点

    while (NULL != p->next)
    {            
        p = p->next;            

        if (data == p->data)
        {
            if (NULL == p->next)
            {
                // 找到的节点是尾节点
                p->prev->next = NULL;
            }
            else
            {
                // 找到的节点是普通节点
                // 前1个节点的next指针指向后一个节点的首地址
                p->prev->next = p->next;
                // 后一个节点的prev指针指向前一个节点的首地址
                p->next->prev = p->prev;

            }
            // 当前节点的prev和next指针都不用管,因为此处会整体销毁整个节点
            free(p);

            return 0;
        }

        // 节点个数减1
        pH->data--;
    }

    printf("can't find the node.\n");
    return -1;
}

int main(int argc, char **argv)
{
    // 创建头指针和头节点并将头指针指向头节点
    struct node *pHeader = creat_node(0);

    // 用于测试向前遍历节点算法
    struct node *tail = NULL;

    // 测试创建节点+头插入节点+向后遍历节点
    insert_head(pHeader, creat_node(11));
    insert_head(pHeader, creat_node(12));
    insert_head(pHeader, creat_node(13));
    traversal_next(pHeader);

    // 测试尾插入节点
    insert_tail(pHeader, creat_node(14));
    insert_tail(pHeader, creat_node(15));
    insert_tail(pHeader, creat_node(16));
    traversal_next(pHeader);

    // 测试向前遍历节点
    tail = pHeader->next->next->next->next; 
    traversal_prev(tail);

    // 测试删除节点
    delete_node(pHeader, 13);
    delete_node(pHeader, 14);
    traversal_next(pHeader);

    return 0;
}

你可能感兴趣的:(linux下c语言)