数据结构与算法之美(笔记2)链表

随机访问

与数组不同,链表如果想要随机访问就不同数组一样了。我们再访问某一个节点的时候,需要从头节点开始一个一个的往下寻找。因此时间复杂度是O(n)。

删除,插入操作

对于链表来说,删除以及插入的时间复杂度是O(1)。但是,我们要删除或者插入的时候,需要找到这个位置,寻找这个位置的过程的时间复杂度就不是O(1)了。

在实际的软件开发中,从链表中删除一个数据无外乎这两种情况:

  • 删除结点中“值等于某个给定值”的结点。
  • 删除给定指针指向的结点。

对于前一种情况来说,无论是单链表还是双向链表都需要从头开始,直到找到给定的值,因此时间复杂度都是O(n)。

而第二种情况来说,我们已经找到了需要删除的结点,但是我们需要找到该结点的前驱结点,如果我们使用的是单链表的话,我们还要重新遍历一次,因此时间复杂度是O(n),双向链表已经记录了前驱结点,所以可以在O(1)内完成。

同理如果我们希望在指定的结点前插入一个数据,双向链表也是如此。

还有就是对于一个有序的链表,我们在查找的时候,使用双向链表可以在上一次的位置,通过数据的大小,决定往前还是往后,这样大概节省了一半的时间。

给出末位删除以及插入的代码实现:

typedef struct Node{
    int data;
    Node* next;
}Node;

class LinkedList{
private:
    Node* head = new Node;// 头指针,这里使用了带头结点的链表,简化实现难度
public:
    LinkedList(){
        head->data = 65535;
        head->next = NULL;
    }
    void insert(int elem){
        Node* newNode = new Node;
        newNode->data = elem;
        newNode->next = NULL;

        newNode->next = head->next;
        head->next = newNode;
    }

    bool Delete(int elem){
        Node* p = head->next;//遍历指针
        Node* ppre = head;// 前驱指针
        for(;p!=NULL;p=p->next){
            if(p->data == elem){
                ppre->next = ppre->next->next;
                return true;
            }
            ppre = p;
        }
        return false;
    }

    void print(){
        Node* p = head->next;
        for(;p!=NULL;p = p->next){
            cout << p->data << endl;
        }
    }
};

空间换时间的设计思想

在内存空间足够的时候,我们可以选择空间复杂度高,但是时间复杂度相对低的算法,相反,如果空间很紧,那么我们就要选择空间复杂度低,时间复杂度高的算法了。这里的链表也是一样,对于数组来说,链表在存储数据的时候,往往要更加消耗空间,因为需要额外的指针变量,如果对于内存比较多的机器来说,我们可以选择双向链表,但对于内存比较吃紧的机器来说,我们优先使用数组。当然,这里的指针变量的消耗是相对来说的,如果说我们存储的数据远远大于指针变量的大小,那么指针变量的大小就可以忽略不计了。

 

利用哨兵简化实现难度

如果说,我们在结点p后面插入一个新的结点,只需要下面两行代码就可解决:

new_node->next = p->next;
p->next = new_node;

但是,如果我们要向一个空链表中插入第一个结点,刚刚的逻辑就不能使用了。我们需要进行下面特殊的处理,其中head表示链表的头结点。所以是这样的:

if (head == null) {
  head = new_node;
}

 我们再来看单链表结点删除操作。如果删除结点p的后继结点,我们只需要一行代码就可以搞定:

p->next = p->next->next;

但是,如果我们要删除链表中最后一个结点(只剩一个结点),前面的删除代码就不ok了。我们要改写成这样子:

if (head->next == null) {
   head = null;
}

如果,我们引入哨兵结点,在任何时候,不管链表是不是空,head指针都会一直指向这个哨兵结点。我们也把这种有哨兵结点的链表叫带头链表。相反,没有哨兵的链表就叫作不带头链表。

 

这样的话,在空链表插入一个结点和删除最后一个结点的逻辑就可以统一了。

 

如何实现单链表反转?

实现的方法有很多,我觉得递归是最直观的了。我么假设一个链表有A,B,C,D,E,五个数据。我们假设BCDE已经反转好了,只要把这个整体指向A,那么问题转化为反转BCDE。如果我们假设CDE已经反转好了,那么问题就转化为反转CDE,以此类推,最后的问题转化为反转E。

这里给出递归代码的实现(这里采用了头结点,所以比较复杂,如果不用头结点就比较简洁了):

    void reverse(){
        if(head->next == NULL || head->next->next == NULL){
            return;
        }
        recur_reverse(head->next);

    }
    Node* recur_reverse(Node* p){
        if(p->next == NULL){
            head->next = p;
            return p;
        }// 递归的终止条件
        Node* newNode = recur_reverse(p->next);
        newNode->next = p;// 将当前的结点指向上一个
        p->next = NULL;
        return newNode->next;// 返回指向的那个结点。
    }

(这里的head是之前定义的类的私有变量)

测试代码:

#include <linkedlist.h>

int main(){
    Linkedlist list;
    list.Insert(10);
    list.Insert(9);
    list.Insert(8);
    list.print();
    list.reverse();
    list.print();
}

如何实现将两个有序的链表结合成一个链表?

跟数组一样,我们创建一个新的链表L3,如何通过两个指针分别指向L1和L2链表,开始遍历,如果哪一个小就插入到新的L3,在某一个链表已经遍历完之后,再将剩下的接上去即可。这里的时间复杂度应该是O(n),空间复杂度也是O(n)。

这里给出代码实现:

typedef struct Node{
    int data;
    Node* next;
}Node,*LinkedList;

// 有序链表合并
void merge(LinkedList L1,LinkedList L2,LinkedList L3){
    while(L1->next != NULL && L2->next != NULL){
        if(L1->next->data <= L2->next->data){
            L3->next = L1->next;
            L1 = L1->next;
        }
        else{
            L3->next = L2->next;
            L2 = L2->next;
        }
        L3 = L3->next;
    }
    if(L1->next != NULL){
        L3->next = L1->next;
    }else{
        L3->next = L2->next;
    }
}

如何实现查找单链表的中间结点

思路:我们使用两个指针指向链表头结点,然后一个为fast,一个为slow,每次fast向前两步,slow向前一步,直到fast到达终点,slow指向的就是我们想要的的中间结点了。这里的时间复杂度是O(n)。然而如果我们使用一个计数的变量,记录这个链表到达终点的次数,然后取这个变量的中点,再遍历到该中点,虽然说这种方法是可行的,但相对第一种来说,遍历的次数就多了。

这里是代码实现(这里的head是之前定义的类的私有变量):

    Node* midNode(){
        if(head->next == NULL){
            return NULL;
        }
        Node* fast = head->next;
        Node* slow = head->next;
        while(fast != NULL && fast->next != NULL){
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;
    }

如何基于链表实现LRU缓存淘汰算法

我们维护一个有序的单链表,越靠近尾部的结点是越早之前访问的。当有一个新的数据被访问时候,我们从头开始顺序遍历链表。

  • 如果这个数据在之前已经缓存在链表中了,我们就把它删除,然后把它插入到头结点之后。
  • 如果这个数据没有在缓存中,这时有两种情况:
  • 如果缓存没有满,那么我们直接把它插入到链表的头部。
  • 如果缓存已经满了,我们把最后一个结点删除,然后把它插入到头部。

我们可以看到,无论是什么情况,我们都需要遍历一遍链表,所以我们缓存访问的时间复杂度是O(n)。实际我们可以使用散列表来记录数据的位置,这样就可以将O(n)复杂度降到O(1)。

如何检测链表中环的存在?

跟找中间结点一样,还是使用fast和slow两个指针,fast向前两步,slow一步,如果存在环,slow到达终点的时候,fast也到达终点了,这里的时间复杂度也是O(n)。

这里给出代码的实现(这里的head是之前定义的类的私有变量):

    bool Check_loop(){
        if(head->next == NULL){
            return false;
        }
        Node* fast = head->next;
        Node* slow = head->next;
        while(fast->next != NULL && fast!=NULL){
            fast = fast->next->next;
            slow = slow->next;

            if(fast == slow) return true;
        }
        return false;
    }

如何实现判断是否是回文字符串(链表存储)?

我们通过使用fast和slow两个指针,fast向前两步,slow一步,直到终点。然后把slow后面的结点进行反转,然后和head到slow之间的数据进行一一比对。这里由于要遍历链表,所以时间复杂度是O(n),空间复杂度是O(1)。

这里给出代码实现(这里的head是之前定义的类的私有变量)

Node* reverse_c1(Node* head,Node* New){
        if(head->next->next == NULL){
            New->next = head->next;
            return New->next;
        }
        Node* new_tail = reverse_c1(head->next,New);
        new_tail->next = head->next;
        head->next->next = NULL;

    }

bool check_huiwen(){
        if(head == NULL || head->next == NULL){
            return false;
        }
        Node* fast = head->next;
        Node* slow = head->next;
        while(fast != NULL && fast->next != NULL){
            fast = fast->next->next;
            slow = slow->next;
        }
        Node* New = new Node;
        reverse_c1(slow,New);
        slow = New;

        for(Node* i=slow->next;i!=NULL;i = i->next,head = head->next){
            if(i->data != head->next->data){
                return false;
            }
        }

        return true;
    }

单链表删除倒数第K个结点

    void Delete_K(int k){
        Node* p = head->next;
        for(;p!=NULL;p = p->next){
            k--;
        }
        if(k>0){
            return;
        }else{
            Node* nP = head->next;
            Node* nPPre = head;
            while(k<0){
                nPPre = nP;
                nP = nP->next;
                k++;
            }
            nPPre->next = nPPre->next->next;
        }
    }

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值