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

随机访问

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

删除,插入操作

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

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

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

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

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

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

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

#include <iostream>

using namespace std;

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


class Linkedlist
{
private:
    Node* head;

public:
    Linkedlist(){
        head = new Node;
        head->next = NULL;
    }
    // 末位插入
    void Insert(int data){
        Node* p = new Node;
        p->data = data;
        p->next = NULL;

        Node* i;
        for(i = head;i->next!=NULL;i = i->next);
        p->next = i->next;
        i->next = p;
    }

    // 删除末位
    void Delete(){
        Node* i;
        if(head->next == NULL){
            cout << "删除失败" << endl;
        }
        for(i = head;i->next->next!=NULL;i = i->next);
        i->next = NULL;
    }

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

空间换时间的设计思想

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

如何实现单链表反转?

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

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

// 翻转
    void reverse(){
        Node* New = new Node;
        reverse_c1(head,New);
        head = New;
    }
    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;

    }

(这里的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是之前定义的类的私有变量):

void mid(){
        if(head->next == NULL || head->next->next == NULL){
            return;
        }
        Node* fast = head->next;
        Node* slow = head->next;
        while(fast->next->next != NULL){
            fast = fast->next->next;
            slow = slow->next;
            if(fast->next == NULL){
                break;
            }
        }
        cout << "中间结点:" << slow->data << endl;
    }

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

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

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

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

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

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

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

bool check_circle(){
        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;

            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;
    }

猜你喜欢

转载自blog.csdn.net/weixin_42073553/article/details/88385141