算法零基础第04天

链表

一、概念

对于顺序表的存储结构,如数组,最大的缺点就是:插入和删除的时候需要移动大量的元素。所以基于前人的智慧,我们有了链表

1、链表定义

链表是有一个个结点构成的,每个结点之间通过链接关系串联起来,每个结点都有一个后继结点,最后一个结点的后继结点为空

 

链表分为单向链表、双向链表、循环链表等

2、结点结构体的定义

typedef int DataType;
struct ListNode {
    DataType data;  // 数据域,可以是任意类型,这里用typedef将其与int同名
    ListNode *next; //指针域,指向后继结点的地址
   
};

一个结点包含两部分:

 

3、结点的创建

ListNode *ListCreateNode(DataType data) {
    ListNode *node = (ListNode *) malloc ( sizeof(ListNode) ); //利用库函数malloc分配一块内存空间,用来存放ListNode链表结点的对象
    node->data = data;                                         //将数据域设置为data
    node->next = NULL;                                         //将指针域设置为空
    return node;                                               //返回结点的指针
}

创建完毕以后

 

二、链表的创建——尾插法

1、算法描述

尾插法:从链表的尾部插入,即记录一个链表尾结点,然后遍历给定的数组,将元素依次插到链表的尾部,每插入一个结点,就将他更新为新的链表尾结点,需要注意的是:链表尾结点的初始情况为空

ListNode *ListCreateListByTail(int n, int a[]) {
    ListNode *head, *tail, *p;         // head春运出头结点的地址,tail存储尾结点的地址,
    int i;                              
    if(n <= 0)
        return NULL;                     //数据元素为空时,返回空链表 
    idx = 0;
    p = ListCreateNode(a[0]);          //创建一个数据域为a[0]的链表结点
    head = tail = p;                   //由于初始情况下只有一个结点,所以将链表头结点head和尾结点tail都设为p
    while(++i < n) {                   //从数组中第一个元素开始,循环遍历数组 
        p = ListCreateNode(a[i]);    //对除第0个元素以外的数据创建结点
        tail->next = p;                //将当前链表的尾结点的后继结点设为p
        tail = p;                      //将最近创建的结点p最为新的尾结点
    } 
    return head;                         //返回链表头结点
} 

三、头插法

1、算法描述

从头结点前面插入元素,这样一来,我们就要将数据元素进行逆序,我们需要逆序访问数组元素达成插入操作

它的代码量会比尾插法少,时间复杂度也很低

ListNode *ListCreateListByHead(int n, int *a) {
    ListNode *head = NULL, *p;       //head存储头结点的地址,初始为空,p存储当前正在插入的结点地址
    while(n--) {                       //总共需要插入n个结点,所以逆序循环n次
        p = ListCreateNode(a[n]);    //创建元素值为a[i]的链表结点
        p->next = head;              //将当前创建的结点的后继结点设为链表的头结点head
        head = p;                    //将链表头节点head设为p 
    } 
    return head;                       //返回头结点
} 
​

四、链表的索引

1、算法描述

给定一个链表头结点head,并且给定一个索引值为i(i>=0),求这个链表的第i个结点,链表的索引就类似于我们玩游戏闯关一样,一关一关的往后,直到你想要闯到的关卡为止,(需要注意的是和数组一样,同样要从下标为0开始)

ListNode *ListGetNode(ListNode *head, int i) {
    ListNode *temp = head;       //temp表示从链表头开始的游标指针,用于遍历操作
    int j = 0;                   //j表示当前访问到的结点
    while(temp && j < i) {       //游标指针部位空并且j<i时,表示还没有访问到目标结点,继续循环
        temp = temp->next;       //将游标指针的后继结点作为新的游标指针
        ++j;                     
    }
    
    if(!temp || j > i) {
        return NULL;             //油表指针为空,或者j>i,说明索引的值超出链表结点,返回空结点
    }
    return temp;                 //最后返回找到的第i个结点
}
​

测试用例

void testListGetNode(ListNode *head) {
    int i;
    for(i = 0; i < 10; ++i) {
        ListNode *node = ListGetNode(head, i);
        if(!node)
            printf("node(%d) :该索引值已超出链表结点数!!\n", i);
        else 
            printf("node(%d) : %d.\n", i, node->data);
    }    
}
int main() {    
    int a[8] = {1,3,5,7,8,10, 2, 6};
    ListNode *head = ListCreateListByHead(8, a);  
    testListGetNode(head);                        
    return 0;
}
​

运行结果

node(0) : 1.
node(1) : 3.
node(2) : 5.
node(3) : 7.
node(4) : 8.
node(9) : 该索引值已超出链表结点数!!.
node(8) : 该索引值已超出链表结点数!!.

索引的时间复杂度最坏的情况下就是遍历整个链表,所以时间复杂度为O(n)

五、链表的查找

链表的查找基本上和索引一样,只是说所以是找下标,查找是找值,都需要遍历链表

ListNode *ListFindNodeByValue(ListNode *head, DataType v) {
    ListNode *temp = head;       //temp表示链表头开始遍历的油表指针
    while(temp) {                //游标指针非空,继续执行
        if(temp->data == values) {
            return temp;         //发现数据域和给定的参数值相等时,返回对应的指针
        } 
        temp = temp->next;       //没有找到则将此时的游标指针作为新的游标指针继续迭代
    }
    return NULL;                 //若遍历结束大批没找到则返回NULL
}

测试用例

void testListFindNodeByValue(ListNode *head) {
    int i;
    for(i = 1; i <= 6; ++i) { 
        ListNode *node = ListFindNodeByValue(head, i); 
        if(!node)
            printf("%d在链表中不存在", i);
        else 
            printf("%d的位置在链表的第%d个位置!!", i,*node);
    }    
}
int main() {    
    int a[5] = {1, 3, 8, 2, 6};
    ListNode *head = ListCreateListByHead(5, a);   
    testListFindNodeByValue(head);
    return 0;
}

运行结果

1的位置在链表的第0个位置!!
8的位置在链表的第2个位置!!
4在链表中不存在.
5在链表中不存在.
​

六、链表结点的插入

1、算法描述

  给定一个链表头head,并且给定一个位置 i(i≥0) 和 一个值 values,求生成一个值为 values 的结点,并且将它插入到 链表 第 i 个结点之后。

首先,我们需要找到第 i 个位置,可以利用上文提到的 链表结点的索引;然后,再执行插入操作,而插入操作分为两步:第一步就是 创建结点 的过程;第二步,是断开之前第 i 个结点 和 第 i+1 个结点之间的 "链",并且将创建出来的结点 "链接" 到两者之间。

ListNode *ListInsertNode(ListNode *head, int i, DataType v) {
    ListNode *pre, *p, *aft;                     //先定义三个指针,当结点插入完毕后, pre -> p -> aft
    int j = 0;                                     // 定义一个计数器,当 j == i时,表明找到要插入的位置
    pre = head;                                    // 从 链表头结点 开始遍历链表
    while(pre && j < i) {                          //如果还没有到链表尾,或者没有找到插入位置则继续循环
        pre = pre->next;                           //将 游标指针 的 后继结点 作为新一轮的 游标指针
        ++j;                                       //计数器加 1
    }
    if(!pre) { 
        return NULL;                               //元素个数不足,无法找到给定位置,返回 NULL
    }
    p = ListCreateNode(values);                       //创建一个值为values的结点
    aft = pre->next;                               
    p->next = aft;                               
    pre->next = p;                           // 这三步就是为了将vtx插入到pre -> aft之间,插入完毕后pre -> p -> aft
    return p;                                    //最后,返回插入的那个结点
}
​

七、链表结点的删除

1、算法描述

  给定一个链表头head,并且给定一个位置i(i≥0),将位置为 i 的结点删除,并且返回新链表的头结点(为什么要返回头结点?因为被删掉的有可能是原来的头结点)。

删除操作分三种情况:

(1) 空链表:无法进行删除,直接返回 空结点;(毕竟人家没有东西,你硬要人家拿出东西这种事情也不可能得对吧) (2) 非空链表删除头结点:缓存下 头结点 的 后继结点,释放 头结点 内存,再返回这个 后继结点; (3) 非空链表删除非头结点:通过遍历,找到 需要删除结点 的 前驱结点,如果 需要删除结点 自身为 空,则返回 链表头结点;否则,缓存 需要删除结点 以及它的 后继结点,将 前驱结点 指向 后继结点,然后再释放 需要删除结点 的内存,返回 链表头结点

ListNode *ListDeleteNode(ListNode *head, int i) {
    ListNode *pre, *del, *aft;
    int j = 0;
    if(head == NULL) {
        return NULL;              //空链表,无法执行删除,直接返回 
    }
    if(i == 0) {                  //需要删除链表第 0 个结点
        del = head;               //缓存第 0 个结点       
        head = head->next;        //将新的 链表头结点 变为 当前头结点 的 后继结点  
        free(del);                //调用系统库函数free释放内存
        return head;              //返回新的 链表头结点
    
    pre = head;                   //从 链表头结点 开始遍历链表
    while(pre && j < i - 1) {     //找到将要被删除结点的 前驱结点pre  
        pre = pre->next;
        ++ j;
    }
    if(!pre || !pre->next) {      //如果 前驱结点 为空,或者 需要删除的结点 为空,则直接返回当前 链表头结点
        return head;
    }
    del = pre->next;              //缓存需要删除的结点到del
    aft = del->next;              //缓存需要删除结点的后继结点到aft
    pre->next = aft;              //将需要删除的结点的前驱结点指向它的后继结点;
    free(del);                    //释放需要删除结点的内存空间
    return head;                  //返回链表头结点
} 
​

八、链表的销毁

1、算法描述

  链表的销毁,就是需要将 所有结点 的内存空间进行释放,并且需要将 链表的头结点 置空。

链表的销毁,可以理解成不断删除第 0 号结点的过程,直到链表头为空位置,只是一个循环调用

void ListDestroyList(ListNode **pHead) { //这里必须用二级指针,因为删除后需要将链表头置空
    ListNode *head = *pHead;             //通过 链表头结点的地址 获取 链表头结点
    while(head) {                        //如果链表非空,则继续循环
        head = ListDeleteNode(head, 0);  //每次迭代,删除 链表头结点,并且返回其 后继结点 作为新的 链表头结点
    }
    *pHead = NULL; //最后,将 链表头结点 置空,这样当函数返回时,传参的head才能是NULL,否则外部会得到一个内存已经释放了的 野指针
}
​

九、链表的优缺点

1、优点

内存分配:   由于是链式存储,随时增加元素随时分配内存,不需要像数组那样进行预分配存储空间; 插入:   当拥有链表某个结点的指针时,在它 后继位置 插入一个新的结点的的时间复杂度为 O(1); 删除:   当拥有链表某个结点的指针时,删除它的 后继结点 的时间复杂度为 O(1);

2、缺点

索引:   索引第几个结点时,时间复杂度为 O(n); 查找:   查找是否存在某个结点时,时间复杂度为 O(n);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值