代码随想录刷题训练营【第三天】

1,649 阅读8分钟

今日任务:

  • 链表理论基础
  • 移除链表元素
  • 设计链表
  • 反转链表

今天的题目我都可以自己写出来,因为链表这里挑的一些题目相对之前的数组来说比较简单一些,但是我写的并不好,因为其实这些题目都是有简单的解法的,但是我没有写出来,我写的在 LeetCode 上面执行消耗的时间都非常高,虽然可以执行。。。,并不是最优解法,连相对优解都算不上,只能说是勉强可以通过。。。不过通过之后观看题解才知道了这些优质的解法,所以这样将自己解题的思路过程,记录一下我想我之后印象会深刻一些

移除链表元素

首先是删除元素,在链表中增删的时间复杂度是 O(1),刚开始想的是直接删除里面的元素即可,但是后来发现,如果直接删除的话需要首先找到我们要删除的元素所在的索引,时间复杂度是 O(n) 同时再遍历到它的前一个结点,然后对这个元素进行删除,同时还要判断他是不是头节点之类的,如果是头节点就需要返回他之后的结点作为新的结点,如果是中间节点就是找到前一个结点指向被删除元素之后的结点,如果是尾部的结点就是需要...... 时间复杂度是 O(n) 结合起来是O(n^2)或者O(2n)?但是还不止如此,因为我们的删除的元素可能不是只有一个,所以 删除链表中元素的思路: 刚开始的一种理解方式就是,遍历到一个结点对这个结点的值进行判断,如果它是符合条件的那么就将他跳过, 如果不符合条件,也就是不是我们要删除的元素就将这个值添加到虚拟头节点之后,也不算是添加而是直接创建一个 因为我当时觉得这样直接添加之后,添加元素之后的 next 关系不好处理,然后就无脑直接重新创建一个,但是这样操作比较耗费内存中的空间,每在虚拟头节点之后添加一个元素之后就需要创建一个新的对象,但是好一点的就是比较好理解。 但是在 leetcode 里面对这里的代码运行之后发现效率并不高,所以这不是最优的解法,于是我参考了题解的解法,发现我和题解的相同之处在于我们都设置了虚拟头节点和一个指向虚拟头节点的指针来作为初始化,但是在移动过程中有所不同 它是直接将 virtualNode.next = listNode 那么这样做就需要处理当listNode 之后存在我们要删除的值得情况 因此就需要将 virtualNode = virtualNode.next 将虚拟头节点指向它得下一个结点,然后当 listNode 的值为我们要删除的结点的时候将 virtualNode 的 Next 指向当前listNode的下一个,先不移动指针,因为我们无法确定后面的不是重复的元素,因此就需要当 listNode 移动之后在进行判断,如果它不是我们删除的结点的话,就将virtualNode 指向它,刚才已经指向过了,然后将 virtualNode 指向它,要是如果我们 virtualNode.next 的值还是我们要删除的元素呢,没关系,我们进入到第一个判断中,继续更新 virtualNode 的 next 指针, virtualNode.next = listNode.next 因为 listNode 是一直移动的,所以我们总可以往后遍历查看,代码比较简单,但是思路有点绕,不是一下子就可以想到的那种

public static Node removeNodeLow(Node node, int target) {
    if (node == null) return node;
    //创建一个没有值的头节点
    Node root = new Node();
    //然后用一个虚拟头指针指向它
    Node virtualPoint = root;
    //开始遍历node链表
    while (node != null) {
        if (node.data == target) {//相等就不添加到后面
            node = node.next;
            continue;
        } else {
            //不相等就将值添加到结点之后
            virtualPoint.next = new Node(node.data);
            virtualPoint = virtualPoint.next;
        }
        node = node.next;
    }
    System.out.println("Man Stop!");
    return root.next;
}

高级解法:

public static Node removeNodePro(Node root, int target) {
    if (root == null) return root;
    Node dummyHead = new Node();
    Node virtualNode = dummyHead;
    while (root != null) {
        if (root.data == target) {
            virtualNode.next = root.next;
        } else {
            virtualNode.next = root;
            virtualNode = virtualNode.next;
        }
        root = root.next;
    }
    System.out.println("Man Stop!");
    return dummyHead.next;
}

构造一个链表

构造一个链表的思路: 刚开始看到这个题目的时候想的是之前数据结构里面学过这个,需要注意的是在对链表进行删除和添加操作的过程中,对不同状态的链表操作进行条件判断 比如添加操作,就有四种, 1、如果链表为空即 size == 0,需要 head = new Node(val); tail = head 2、添加的是头部节点 3、添加的是尾部节点 4、添加的是中间节点 过程类似和下面的类似,主要是需要注意考虑这些条件的判断,都要写出来的,不然很容易就导致空指针异常了,其它的我觉得还好,构造链表算是送分题吧,别把它做成送命就行🤡

如果是删除操作的话,也是要考虑四种情况 0、如果删除的时候链表为 1,删除之后链表就需要同时更新 head 和 tail head = null tail = null 1、然后如果删除尾部节点 遍历到达尾节点之前的元素,然后删除尾节点之后将它设置为新的尾节点 2、删除头部节点 将 newHead = head.next head.next = null head = newHead; 4、删除中间节点 需要设置 for 循环将指针移动到我们要删除的元素之前的位置,然后 deleteNode = pre.next pre.next = pre.next.next deleteNode.next = null;

class MyLinkedList {

    public MyLinkedList() {

    }
    class LinkedList {
        LinkedList next;
        int val;

        public LinkedList() {
            next = null;
            val = 0;
        }

        public LinkedList(int val) {
            this.val = val;
        }
    }

    LinkedList head;
    LinkedList tail;
    int size;
    public int get(int index) {
        if (index < 0 || index > size - 1) return -1;
        //如果索引有效的话就向后面进行遍历
        LinkedList temp = head;
        int ret = 0;
        while (index-- >= 0) {//不能直接操作head需要定义一个额外的指针 temp 来获取到对应的数据
            ret = temp.val;
            temp = temp.next;
        }
        return ret;
    }
    
    public void addAtHead(int val) {
        if (size == 0) {
            head = new LinkedList(val);
            tail = head;
        } else {
            LinkedList newNode = new LinkedList(val);
            newNode.next = head;
            head = newNode;
        }
        size++;
    }
    
    public void addAtTail(int val) {
        if (size == 0) {
            head = new LinkedList(val);
            tail = head;
        } else {
            LinkedList newNode = new LinkedList(val);
            tail.next = newNode;
            tail = newNode;
        }
        size++;
    }
    
    public void addAtIndex(int index, int val) {
        if (index <= 0) {
            LinkedList newNode = new LinkedList(val);
            newNode.next = head;
            head = newNode;
            size++;
        } else if (index == size) {
            //如果index大于链表的长度则不会添加
            LinkedList newNode = new LinkedList(val);
            tail.next = newNode;
            tail = newNode;
            size++;
        } else if (0 < index && index < size) {
            //在中间的情况
            LinkedList newNode = new LinkedList(val);
            LinkedList temp = head;
            for (int i = 0; i < index - 1; i++) {//遍历到我们要添加位置的前面
                temp = temp.next;
            }
            newNode.next = temp.next;
            temp.next = newNode;
            size++;
        }
    }   
    
    public void deleteAtIndex(int index) {
        //判断如果index无效直接返回即可
        if (index < 0 || index >= size) return;
        //然后对删除的节点的位置分情况进行判断
        if (size == 1) {
            tail = null;
            head = null;
        } else if (index == 0) {//删除头节点
            LinkedList newHeader = head.next;
            head = newHeader;
        } else if (index == size - 1) {//删除尾节点
            //遍历到达最后一个节点的前一个结点
            LinkedList temp = head;
            for (int i = 0; i < size - 2; i++) {//1 2
                temp = temp.next;
            }
            temp.next = null;
            tail = temp;
        } else {//剩下的情况就是删除中间节点
            //删除中间节点的关键是遍历到达要删除的结点之前的结点
            LinkedList temp = head;
            for (int i = 0; i < index - 1; i++) {//1 2 3
                temp = temp.next;
            }
            temp.next = temp.next.next;
        }
        size--;
    }
}

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * MyLinkedList obj = new MyLinkedList();
 * int param_1 = obj.get(index);
 * obj.addAtHead(val);
 * obj.addAtTail(val);
 * obj.addAtIndex(index,val);
 * obj.deleteAtIndex(index);
 */

反转链表

反转链表,这个题目我之前见别人做过,有双指针的也有递归的,递归的我忘完了所以我想了想迭代的方式,好像是将后面节点的指针指向之前的,所以我就定义了一个循环 while 来将 temp = head 的 temp 指针遍历到当前链表的最后一个位置 然后如果 temp.next.next == null 就说明我们已经到了 最后一个节点的前一个了,这时候temp.next获取后面的节点,然后将他的 next 设置为之前的, 然后temp = head; 此时的 head 已经是被我们修改之后的链表了 比如说之前的链表是 1 -> 2 -> 3 -> 4 那么第一次反转之后,就变成了 1 -> 2 -> 3 <- 4 因此我判断最外层循环结束的条件就是当 1 <- 2 <- 3 <- 4 这样就是 head.next = null 的时候代表我们已经将整个链表反转完毕,就退出,不过这里面也有一个问题就是我们返回的是 head 所以还要获取到反转之后的节点的头节点,所以加了一个 index 的判断,当第一次进入到 while 循环当中时,就把这个尾节点保存下来,它是我们反转之后的节点的头节点 其实我想在想来,这个思路是当时看 Youtube 上面那个递归系列题里面他给出的递归代码的一个迭代实现,我记得它的递归代码就是从后往前进行遍历然后更新的, 不过这个迭代我在写的时候也感觉有点不对劲,因为每次循环都需要将没有反转的链表元素遍历一遍,很麻烦的,但是从头开始反转链表我又不会,只能硬着头皮把代码写完了,之后看题解发现看不懂。。虽然它的代码很简洁,思路也很好,不过我最后看了看Carl的视频里面的演示算是明白了,它是使用双指针的方式来处理的 算法的时间复杂度也就是个 O(n) 比我的好多了。。。,看来指针的用法真的很奇妙,不过需要注意的一点是 temp 是用来存储下一个元素的位置,因为当我们将当前 cur 的next指向pre的时候它的next已经改变了,这时候是无法回到后面的,因此只有先将它保存下来,还有 prev 的移动也很巧妙,是在 cur 移动之前跑到它的位置,And So onnnnn, 知道 cur 为空将整个链表反转,然后返回 prev 即可。

public static LinkedList doReverse(LinkedList node) {
    if (node == null || node.next == null) return node;
    LinkedList temp = node;
    int index = 0;
    LinkedList reverseHead = null;
    while (temp.next != null) {
        //也就是 head 的下一位不是空,可以进入到这个位置的结点就至少拥有两个结点
        while (temp.next.next != null) {
            temp = temp.next;
        }
        if (index++ == 0) {
            reverseHead = temp.next;
        }
        temp.next.next = temp;
        temp.next = null;
        temp = node;
    }
    System.out.println("Man Stop!");
    return reverseHead;
}

高级解法:

public static LinkedList reverseListPro(LinkedList head) {
    LinkedList temp = null;
    LinkedList prev = null;
    LinkedList cur = head;
    while (cur != null) {
        temp = cur.next;
        cur.next = prev;
        prev = cur;
        cur = temp;
    }
    return prev;
}

总结:

反转链表最好画图理解(双指针的应用真奇妙!)

构造数组注意条件的判断

删除元素使用虚拟头节点来做好一些