数据结构以学带练day3——链表、对链表的操作、虚拟头节点、反转链表

472 阅读7分钟

链表基础知识

分类

单链表

链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。链表的入口节点称为链表的头结点也就是head。

image.png

双链表

每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。双链表既可以向前查询也可以向后查询。

image.png

循环链表

链表首尾相连。循环链表可以用来解决约瑟夫环问题。

image.png

链表的存储

链表是通过指针域的指针链接在内存中各个节点。所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。

image.png

C/C++的定义链表节点方式及初始化

// 单链表
struct ListNode {
    int val;  // 节点上存储的元素
    ListNode *next;  // 指向下一个节点的指针
    ListNode(int x) : val(x), next(NULL) {}  // 节点的构造函数
};

通过自己定义构造函数初始化节点:

ListNode* head = new ListNode(5);

使用默认构造函数初始化节点:

ListNode* head = new ListNode();
head->val = 5;

链表的操作

删除节点

image.png

只要将C节点的next指针 指向E节点就可以了。在C++里最好是再手动释放这个D节点,释放这块内存。

添加节点

image.png

链表断链的时候如果后面还需要该被断开的节点,应当用指针临时存储一下。

链表的增添和删除都是O(1)操作,也不会影响到其他节点。

注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。

链表与数组对比

image.png

链表的插入/删除时间复杂度是O(1)是因为已经知道前一个节点的情况下,如果单纯给一个链表删除特定元素,那么需要遍历+删除,时间复杂度就是O(n)了。

题目

203.移除链表元素

image.png

!虚拟头节点法

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
        dummyHead->next = head; // 将虚拟头结点指向head,这样方面后面做删除操作
        // 上面两句可以直接换成 : 
        // ListNode* dummyHead = new ListNode(0,head);
        ListNode* cur = dummyHead;
        while (cur->next != NULL) {
            if(cur->next->val == val) {
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;//!!!删除操作
                delete tmp;   //释放内存
            } else {
                cur = cur->next;
            }
        }
        head = dummyHead->next;
        delete dummyHead;    //释放内存
        return head;
    }
};
  • 关于链表定义和实例化

根据上面代码注释部分可以看出,链表定义首先必须包含:一个数值变量int val和一个指针变量ListNode *next

其次可以有三种重构方式,在实例化时:

  1. ListNode* dummyHead = new ListNode():表示该节点值为0,指向NULL;

  2. ListNode* dummyHead = new ListNode(5):表示该节点值为5,指向NULL;

  3. ListNode* dummyHead = new ListNode(5,head):表示该节点值为0,指向head。

  • 关于cur指针
  1. cur是个ListNode类型的指针

  2. 不能直接用head节点进行后续的操作,如果head节点的值就是要删的值,用head节点操作没法删除自己,必须要找到head的前一个节点。因此也解释了为什么ListNode* cur = dummyHead;

  • C++最好要释放内存

语法即 delete XXX;

分头节点和非头节点的方法

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        // 删除头结点
        // 注意这里不是if,因为有可能后面都是要删除的val:
        // 例如链表为[1,1,1,2,3,4,5],val=1,则删除头节点这个操作要执行3次
        while (head != NULL && head->val == val) { 
            ListNode* tmp = head;
            head = head->next;
            delete tmp;
        }

        // 删除非头结点
        ListNode* cur = head;
        while (cur != NULL && cur->next!= NULL) {
            if (cur->next->val == val) {
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp;
            } else {
                cur = cur->next;
            }
        }
        return head;
    }
};

707.设计链表(增删改查)

image.png

class MyLinkedList {
//这三句不能放在struct ListNode的前面,否则ListNode* myhead;无法执行
// private:
//     int size;
//     ListNode* myhead;
public:
    //定义单链表结构体
    struct ListNode {
      int val;
      ListNode *next;
      
      //三种重构方式
      ListNode() : val(0), next(nullptr) {}
      ListNode(int x) : val(x), next(nullptr) {}
      ListNode(int x, ListNode *next) : val(x), next(next) {}
    };
    //构造函数
    MyLinkedList() {
        //定义虚拟头节点
        myhead = new ListNode(0);
        //!!!链表长度
        size = 0;
    }
    //得到链表中index的值
    int get(int index) {
        // 获取到第index个节点数值,如果index是非法数值直接返回-1, 
        //!!注意index是从0开始的,第0个节点就是头结点
        //因此假如size=8,但索引只能取到7,所以index>=8是无效的
        if(index >= size || index<0){
            return -1;
        }
        //从index=0开始,即从头节点开始
        ListNode *cur = myhead ->next;
        while(index--){
            cur =  cur->next;
        }
        return cur->val; 
    }
    //在链表头添加节点
    void addAtHead(int val) {
        ListNode * newnode = new ListNode(val);
        //注意:myhead是虚拟头节点
        //把新插入的节点指向 myhead节点 所指向的节点,即head节点
        newnode->next = myhead -> next; 
        //再把myhead指向这个新插入的头节点
        myhead ->next = newnode;
        //链表长度+1
        size++;
    }
    //在链表尾添加节点
    void addAtTail(int val) {
        ListNode * newnode = new ListNode(val);
        ListNode* cur = myhead;
        //让cur指向最后一个节点
        while(cur->next!=nullptr){
            cur = cur -> next;
        }
        //结束循环,则代表cur已经指向最后一个节点了
        //此时把cur的下一个节点指向newnode,即代表在尾部添加节点
        cur->next = newnode;
        size++;
    }
    //在链表的index处添加节点
    void addAtIndex(int index, int val) {
        //判断index和链表长度关系,index超过链表长,则不返回任何
        if(index > size){return;}
        //index小于0在头部插入,即把index置为0
        if(index < 0) index = 0;
        ListNode * newnode = new ListNode(val);
        ListNode* cur = myhead;
        //index不为0
        while(index--){
            cur = cur->next;
        }
        //如果index=0或者while循环结束
        newnode->next = cur->next;
        cur -> next = newnode;
        size++;
    }
    //删除链表index处的节点
    void deleteAtIndex(int index) {
        //如果index无效,则不做操作
        if(index >= size || index<0){
            return;
        }
        
        ListNode *cur = myhead;
        while(index--){
            cur = cur -> next;
        }
        ListNode * temp = cur->next;//用于释放内存
        cur->next = cur->next->next;
        delete temp;
        size--;
    }
    // 打印链表
    void printLinkedList() {
        ListNode* cur = myhead;
        while (cur->next != nullptr) {
            cout << cur->next->val << " ";
            cur = cur->next;
        }
        cout << endl;
    }
private:
    int size;
    ListNode* myhead;
};

//用于测试
//  MyLinkedList* obj = new MyLinkedList();
//  int param_1 = obj->get(index);
//  obj->addAtHead(val);
//  obj->addAtTail(val);
//  obj->addAtIndex(index,val);
//  obj->deleteAtIndex(index);

206.反转链表

image.png

链表一定要分清节点和指针的概念。 new ListNode()是真实存在的一个节点

head = new ListNode() 相当于 head指针指向了一个真实的节点

node = head, 相当于node和head同时指向了这个真实的节点

ListNode *cur = head;相当于定义了cur指针,指向了head。

双指针法

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        //定义三个指针
        ListNode *pre = nullptr;//pre指向空
        ListNode *cur = head;//cur指向head节点
        ListNode* temp; // 用于保存cur的下一个节点
        //循环结束cur会指向null,pre指向最后的节点
        while(cur!=nullptr){
            temp = cur->next; //保存cur的下一个节点
            cur->next = pre; // 翻转操作
            // 更新pre 和 cur指针
            pre = cur; //把pre指向  cur所指向的节点
            cur = temp;//把cur指向  temp所指向的节点
        }//当cur指向最后一个节点,再进入while循环后,cur最终指向null
        return pre;//所以最后应该返回pre,而不是cur
    }
};

f406b0f15e5fd3341da67ec59c01ee0.jpg

递归法

根据双指针的思路可以写出递归的方法:

  • 定义递归反转的函数reverse(),根据双指针法,要反转的是cur和pre两个指针,即在第一次递归中的head和NULL,则传入的为reverse(NULL, head);
  • 在reverse()函数中,递归的终止条件是cur指向了最后的null,按照题目要求返回反转后的头节点,即pre
class Solution {
public:
    ListNode* reverse(ListNode* pre,ListNode* cur){
        //递归的终止条件是cur指向了最后的null,按照题目要求返回反转后的头节点,即pre
        if(cur == NULL) return pre;
        
        ListNode* temp = cur->next;
        cur->next = pre;//反转操作
        // 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
        // pre = cur;
        // cur = temp;
        return reverse(cur,temp);
    }
    ListNode* reverseList(ListNode* head) {
        // 和双指针法初始化是一样的逻辑
        // ListNode* cur = head;
        // ListNode* pre = NULL;
        return reverse(NULL, head);
    }

};

总结

今日学习时长 3+1h