【代码随想录第三天】

代码随想录算法训练营第三天 | 203.移除链表元素 707.设计链表 206.反转链表

第一次试着写博客,随便写写。

题目一:移除链表元素

删除链表中等于给定值 val 的所有节点。请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

示例 1: 输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]
示例 2: 输入:head = [], val = 1 输出:[]
示例 3: 输入:head = [7,7,7,7], val = 7 输出:[]

本题可以创建一个哨兵节点temphead(保存值为-1),让temphead的next指向head。
由于需要实际变化过后的head,因此返回temphead.next而不是不动的head.
使用两个指针before和 p 一前一后移动,删除元素时用before和p.next跳过p指向的节点。

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        if(head == null)
        {
            return head;
        }
        ListNode temphead = new ListNode(-1,head);
        ListNode p = head;
        ListNode before = temphead;
        while(before.next != null)
        {
            if(val == p.val)
            {
                before.next = p.next;
            }else{
                before = p;
            }
            p = p.next;
        }
        return temphead.next;
    }
}

注意:
c++在堆区申请内存后仍需delete手动回收,而java中有虚拟机内存回收机制,不用手动回收。
c++会使用 (*p).next 或 p->next 的形式解引用,而java是在调用对象,直接 p.next 即可。
(用java而不是c++之后,感觉以前写链表用c++被指针和引用搞的云里雾里,用java结果能理解记住了,真的奇怪)

题目二:链表实现(java,单链表)

实现过程没有什么大坑,不过有几点需要注意:
1.可以将节点和列表实现用两个类分开,有助于代码简洁;
2.数组可通过下标查找或遍历元素,java的链表遍历可直接在对象中插入index属性以计数。
同时,这也意味着,可以不用靠节点指针或者迭代器遍历链表,直接for循环就行。
3.无论是头插法还是尾插法,都可统一成相同的插入方法而使用不同的参数。可以调用另外的插入函数。
4.注意index边界和空指针问题。
(个人实现时出现了大问题,就不贴出来了,下面是标答)

//单链表
class ListNode {
    int val;
    ListNode next;
    ListNode(){}
    ListNode(int val) {
        this.val=val;
    }
}
class MyLinkedList {
    //size存储链表元素的个数
    int size;
    //虚拟头结点
    ListNode head;

    //初始化链表
    public MyLinkedList() {
        size = 0;
        head = new ListNode(0);
    }

    //获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点
    public int get(int index) {
        //如果index非法,返回-1
        if (index < 0 || index >= size) {
            return -1;
        }
        ListNode currentNode = head;
        //包含一个虚拟头节点,所以查找第 index+1 个节点
        for (int i = 0; i <= index; i++) {
            currentNode = currentNode.next;
        }
        return currentNode.val;
    }

    //在链表最前面插入一个节点,等价于在第0个元素前添加
    public void addAtHead(int val) {
        addAtIndex(0, val);
    }

    //在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
    public void addAtTail(int val) {
        addAtIndex(size, val);
    }

    // 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
    // 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
    // 如果 index 大于链表的长度,则返回空
    public void addAtIndex(int index, int val) {
        if (index > size) {
            return;
        }
        if (index < 0) {
            index = 0;
        }
        size++;
        //找到要插入节点的前驱
        ListNode pred = head;
        for (int i = 0; i < index; i++) {
            pred = pred.next;
        }
        ListNode toAdd = new ListNode(val);
        toAdd.next = pred.next;
        pred.next = toAdd;
    }

    //删除第index个节点
    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) {
            return;
        }
        size--;
        if (index == 0) {
            head = head.next;
        return;
        }
        ListNode pred = head;
        for (int i = 0; i < index ; i++) {
            pred = pred.next;
        }
        pred.next = pred.next.next;
    }
}

题目三:206反转链表

如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。
其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表.

class Solution 
{
    //iteration
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;
        ListNode temp = null;
        while(cur != null)
        {
            temp = cur.next;//预先指向后一个节点
            cur.next = pre;
            pre = cur;
            cur = temp;
        }
        return pre;
    }
}

用迭代法实现反转。
注意这里实际用了三个指针实现反转:
pre:previous,指前面指针; cur:current,指当前指针;
pre和cur是双指针,让pre一上来指向空而不是head可以规避特殊情况的判定。(确实一上来让pre指向head还挺麻烦的)
temp得提前保存cur原来应该指向的下一个节点(不然cur没的指向了)。
最后返回pre,为新链表的头节点。此时cur指向空。

注意迭代法的一次迭代:每次判定cur不为空,之后进行pre和cur赋值的操作,将cur赋给pre,temp赋值给cur.这是每一次迭代要求的,也是每一层递归时应该等价实现的内容。

用递归法实现反转。

class Solution 
{
    //recursion
    public ListNode reverseList(ListNode head) {
	    return reverse(null, head);
	    //起始条件,剩下的交给reverse递归解决
    }
    public ListNode reverse(ListNode pre, ListNode cur)
    {
	    if(cur == null)//边界条件
	    {
		    return pre;
	    }
	    ListNode temp = cur.next;
	    cur.next = pre;
	    //如下递归的写法,其实就是做了这两步
        // pre = cur;
        // cur = temp;
		reverse(cur, temp);
		//理解这一步是关键
    }
}

唯一的关键是reverse()嵌套调用reverse时。里面的reverse()功能对标了迭代法的每一次迭代赋值。迭代时将cur赋给pre,temp赋值给cur,实质实现的是节点交换顺序。同样,每次调用reverse时也同样在实现此功能,实现交换。因此子递归调用reverse(),形参为cur和temp,将它们实现交换(实质我们也确实在这样做)。
时间复杂度: O(n), 递归处理链表的每个节点
空间复杂度: O(n), 递归调用了 n 层栈空间

还有另外一种与双指针法不同思路的递归写法:从后往前翻转指针指向。很有意思。

// 从后向前递归
class Solution 
{
    ListNode reverseList(ListNode head) {
        // 边缘条件判断
        if(head == null) 
	        return null;
        if (head.next == null) 
	        return head;
        
        // 递归调用,翻转第二个节点开始往后的链表
        ListNode last = reverseList(head.next);
        // 翻转头节点与第二个节点的指向
        head.next.next = head;
        // 此时的 head 节点为尾节点,next 需要指向 NULL
        head.next = null;
        return last;
    } 
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值