关于链表的一些逻辑思维

写这个东西的原因

  • 大学也即将毕业了,很多算法和数据结构的东西可能在iOS开发中并不能用得上,曾经有颗去BAT的心,错过了校招,希望厚积薄发,两三年后或许还有机会。记录下,那些大学里为之痴迷的东西。

什么是链表?

  • 对于计算机而言,大多数数据和信号的存储方式是离散的。那么今天要介绍的链表,就是一种离散结构的数据结构。与之对应的数组,是一种连续的数据结构。
  • 先来看一张相对形象的图。


    关于链表的一些逻辑思维_第1张图片
    list.jpg

定义链表的节点

  • 由于链表是由多个链表节点,连接而成。先看看如何定义一个节点吧,其实就是个结构体。
typedef struct Node {
     int val;                  //数据域
     struct Node *next;        //指针域
}pNode;
  • 一个节点中包含数据域和指针域,数据域用于存储节点的数据信息,而指针next则指向下一个节点的地址。从内存的角度来看,它便是离散的,因为指向的下一块内存不是连续的。

如何创建单链表

  • 一条单链表中,必不可少的是头节点,相当于一个标记。个人习惯在头节点保存链表节点的个数
//creat a list
inline Node *creat() {
     Node *head = (Node *)malloc(sizeof(Node *));
     head->next = NULL; //防止野指针
     return head;
}
  • 关于malloc(),我们可以通过这个函数对头节点分配堆上的内存空间用于存储节点信息。而sizeof关键字就是计算需要分配多少的空间了。这个地方存在一个字节对齐的问题。感兴趣的同学可以深入理解。

链表的插入

  • 头插法,效率高,也是C++ STL中使用的一种方式。
  • 尾插法,效率低,只是介绍下,毕竟还是有区别的。

头插法
  • 我们可以猜测,既然是头部插入,那我们插入的元素顺序就是逆序的。
//头插法,时间复杂度O(1)
inline void push_front(Node *head, const int _val) {
    if (!head) return ;  //判断链表是否存在,通过头指针是否为空
    Node *nNode = (Node *)malloc(sizeof(Node *));
    nNode->val = _val;
    Node *pNode = head->next;
    nNode->next = pNode;
    head->next = nNode;
    ++head->val;//统计节点个数
}
尾插法
  • 既然是尾部插入,那我们插入的元素顺序就是顺序的。
27 //尾插法,时间复杂度O(n)
 inline void push_back(Node *head, const int _val) {
     if (!head) return ;
     Node *nNode = (Node *)malloc(sizeof(Node *));
     nNode->val = _val;
     Node *pNode = head;
     while (pNode->next) {
         pNode = pNode->next;
     }
     pNode->next = nNode;
     nNode->next = NULL;
     ++head->val;//统计节点个数
 }
  • 我们不难发现,之所以时间复杂度为O(n),是因为,每次比头插法多花了一个while循环来遍历链表,直到找到链表的最后一个节点。

链表删除节点

  • 任何一种数据结构,必不可少的都包含增删查改的功能。删除也是链表操作中较为复杂的一个操作。时间复杂度即为查找删除节点的遍历的复杂度。
//删除链表中第一个值为_val的节点
inline void remove(Node *head, const int _val) {
    if (!head || !head->next) return ;
    Node *pNode = head;
    while (pNode->next) {
        if (pNode->next->val == _val) {
            //找到要删除的元素
            Node *dNode = pNode->next;
            pNode->next = dNode->next;
            if (!dNode->next) {
                 pNode->next = NULL;
            }
            free(dNode);
            --head->val;//更新节点统计
            break;
        }
        pNode = pNode->next;
    }
}

关于链表的一些巧妙应用

  • 偷懒:至于什么判断链表是不是为空,链表节点个数,或者查找链表中某个元素是否存在的函数,我就懒得介绍了。接下来看几个有趣的应用。

如何判断单链表是否存在自环
  • 这是前两年大公司非常喜欢问的一道题目,何谓自环?就是在链表中形成了一个循环。
  • 对于这个问题,有一个很棒的思路, 那就是采用两个指针,一个指针从head开始,另一个从head->next开始,第一个指针每次只走一步,第二个指针每次走两步。正常不存在环的情况下,以第二个指针到达尾节点作为判断的依据,若是存在环,那必定这两个指针会相遇,那么指针相遇便能作为判断单链表中存在环的依据。
//判断单链表中是否存在自环 1代表存在,0则反之。
inline int judge_circle(Node *head) {
    if (!head || !head->next) return 0;
    Node *first = head->next, *second = head->next->next;
    while (second && first) {
        if (first == second) return 1;
        second = second->next->next;
        first = first->next;
    }
    return 0;
}
  • 该算法的时间复杂度是O(n)的。也就是最多只会遍历一次链表。附上Leetcode题目地址:https://leetcode.com/problems/linked-list-cycle/

翻转整条单链表

  • 操作结果便是将 1->2->3->4->NULL 转换为 4->3->2->1->NULL.
  • 思路很简单,无非就是指针操作,采用两个指针反复修改指向就可以了。Leetcode题目地址:https://leetcode.com/problems/reverse-linked-list/
//翻转整条链表,时间复杂度为O(n)
inline Node *reverse(Node *head) {
    if (!head || !head->next) return head;
    Node *pNode = head, *qNode = pNode->next;
    pNode->next = NULL;
    if (!qNode->next) {
        head = qNode;
        qNode->next = pNode;
        return head;
    }
    while (qNode->next) {
        head = qNode->next;
        qNode->next = pNode;
        pNode = qNode;
        qNode = head;
    }
    head->next = pNode;
    return head;
}

总结:先记录这么多吧,还有些题目暂时还没时间去整理出来。接下来有时间会持续更新。

你可能感兴趣的:(关于链表的一些逻辑思维)