链表(数据结构与算法java代码)

第一题 移除链表元素

点击跳转到力扣

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

注意

在C和C++中,内存管理通常是手动进行的。这意味着开发者需要显式地分配和释放内存。当一个对象不再需要时,必须使用相应的方法(如C中的free()函数或C++中的delete操作符)来释放该对象占用的内存空间,否则会导致内存泄漏。

相比之下,Java和Python都采用了自动内存管理机制,即垃圾收集器(Garbage Collector,GC)。垃圾收集器会自动跟踪哪些内存块不再被使用,并自动回收这些内存,使得程序员不需要手动管理内存的分配和释放。

设置虚拟头结点

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeElements(ListNode head, int val) {
        if (head == null) {
            return null;
        }
        // 设置虚拟头结点,next指向真正的头结点,是为了处理要删除头结点这种情况
        ListNode dummy = new ListNode(-1, head); 
        ListNode pre = dummy;
        ListNode cur = head;

        while (cur != null) {
            if (cur.val == val) {
                pre.next = cur.next; // 若出现要删的节点,跳过
            } else {
                pre = cur;
            }
            cur = cur.next;
        }
        return dummy.next; // head可能被删
    }
}

不添加虚拟节点方式(双指针)

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        // 移到第一个遇到的不需要删除的节点
        // head为null,跳过
        // 头结点不需要被删,跳过
        while (head != null && head.val == val) {
            head = head.next; 
        }
        // head为null,返回
        if (head == null) {
            return head;
        }
        ListNode pre = head;
        ListNode cur = head.next;
        while (cur != null) {
            if (cur != val) {
                pre = cur;
            } else {
                pre.next = cur.next;
            }
            cur = cur.next;
        }
        return head;
    }
}

不添加虚拟节点方式(单指针)

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        while (head != null && head.val == val) {
            head = head.next; 
        }
        ListNode cur = head;
        while (cur != null) {
            // null.val 会报错
            // 若cur的下一位等于val,直接跳过
            while (cur.next != null && cur.next.val == val) {
                cur.next = cur.next.next;
            }
            cur = cur.next;
        }
        return head;
    }
}

第二题 设计链表

点击跳转到力扣

你可以选择使用单链表或者双链表,设计并实现自己的链表。

单链表中的节点应该具备两个属性:val 和 next .val 是当前节点的值,next 是指向下一个节点的指针/引用。

如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。

实现 MyLinkedList 类:

  • MyLinkedList() 初始化 MyLinkedList 对象。
  • int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。
  • void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
  • void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
  • void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。
  • void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。

 单链表

class MyLinkedList {
    int size; // 链表长度,index + 1
    ListNode dumyhead; // 虚拟头结点

    // 初始化链表
    // size = 0 表示虚拟头结点
    public MyLinkedList() {
        size = 0;
        dumyhead = new ListNode(-1);
    }
    
    // 获取索引为index的值
    // index = 0 表示头结点
    public int get(int index) {
        if (index < 0 || index >= size) {
            return -1;
        }
        ListNode cur = dumyhead;
        // 从虚拟头结点开始后移动,移一次到达index = 0 的头结点
        // 再移动index次到达索引为index的节点
        for (int i = 0; i <= index; i++) {
            cur = cur.next;
        }
        return cur.val;
    }
    
    // 添加头结点
    public void addAtHead(int val) {
        addAtIndex(0, val);
    }
    
    // 添加尾节点
    public void addAtTail(int val) {
        addAtIndex(size, val);
    }
    
    // 在索引为index的节点前添加节点
    public void addAtIndex(int index, int val) {
        if (index > size) {
            return;
        } 
        if (index < 0) {
            index = 0;
        }
        size++;
        // index == size时,相当于添加尾节点
        ListNode pre = dumyhead;
        for (int i = 0; i < index; i++) {
            pre = pre.next; 
        } // 移动到index前一位
        ListNode newNode = new ListNode(val);
        newNode.next = pre.next;
        pre.next = newNode;
    }
    
    // 删除索引为index的节点
    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) {
            return;
        }
        size--;
        ListNode pre = dumyhead;
        for (int i = 0; i < index; i++) {
            pre = pre.next;
        }
        pre.next = pre.next.next;
    }
}

双向链表

其中的添加和删除均尝试过改动,但是自己设想没问题,一运行就有问题

class MyLinkedList {
    int size; // 链表长度,index + 1
    ListNode dumyhead, dumytail; // 虚拟头,尾结点

    class ListNode {
        int val;
        ListNode next, prev;
        ListNode(){}
        ListNode(int val) {
            this.val = val;
        }
    }

    // 初始化链表
    // size = 0 表示虚拟头结点
    public MyLinkedList() {
        size = 0;
        dumyhead = new ListNode(0);
        dumytail = new ListNode(0);
        // 这一步非常关键,否则在加入头结点的操作中会出现null.next的错误
        dumyhead.next = dumytail;
        dumytail.prev = dumyhead;
    }
    
    // 获取索引为index的值
    // index = 0 表示头结点 index = size 表示虚拟尾节点
    public int get(int index) {
        if (index < 0 || index >= size) {
            return -1;
        }
        // 移到index处节点
        ListNode cur = this.dumytail;
        if (index >= size / 2) {
            for (int i = size; i > index; i--) {
                cur = cur.prev;
            }
        } else {
            cur = this.dumyhead;
            // 因为头结点索引是0,所以比倒序要多移一次
            for (int i = 0; i <= index; i++) {
                cur = cur.next;
            }
        }
        return cur.val;
    }
    
    // 添加头结点
    public void addAtHead(int val) {
        addAtIndex(0, val);
    }
    
    // 添加尾节点
    public void addAtTail(int val) {
        addAtIndex(size, val);
    }
    
    // 在索引为index的节点前添加节点
    public void addAtIndex(int index, int val) {
        if (index > size) {
            return;
        } 
        if (index < 0) {
            index = 0;
        }
        size++;
        ListNode pre = this.dumyhead;
        for(int i = 0; i < index; i++){
            pre = pre.next;
        }
        ListNode newNode = new ListNode(val);
        newNode.next = pre.next;
        pre.next.prev = newNode;
        newNode.prev = pre;
        pre.next = newNode;
    }
    
    // 删除索引为index的节点
    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) {
            return;
        }
        size--;
        ListNode pre = this.dumyhead;
        for(int i=0; i<index; i++){
            pre = pre.next;
        }
        pre.next.next.prev = pre;
        pre.next = pre.next.next;
    }
}

/**
 * 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);
 */

未解决的错误

1. 报错空指针异常

java.lang.NullPointerException: Cannot read field "next" because "<local3>" is null at line 78, MyLinkedList.addAtIndex

 // 在索引为index的节点前添加节点
    public void addAtIndex(int index, int val) {
        if (index > size) {
            return;
        } 
        if (index < 0) {
            index = 0;
        }
        size++;
        // 求index前一位
        ListNode pre = this.dumytail;
        if (index >= size / 2) {
            for(int i = size; i >= index; i--){
                pre = pre.prev;
            }
        } else {
            for(int i = 0; i < index; i++){
                pre = pre.next;
            }
        }
        ListNode newNode = new ListNode(val);
        newNode.next = pre.next;
        pre.next.prev = newNode;
        newNode.prev = pre;
        pre.next = newNode;
    }

2. 无法正常删除元素

    // 删除索引为index的节点
    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) {
            return;
        }
        size--;
        ListNode cur = this.dumytail;
        if (index >= size / 2) {
            for (int i = size; i > index; i--) {
                cur = cur.prev;
            }
        } else {
            cur = this.dumyhead;
            // 因为头结点索引是0,所以比倒序要多移一次
            for (int i = 0; i <= index; i++) {
                cur = cur.next;
            }
        }
        cur.prev.next = cur.next;
        cur.next.prev = cur.prev;
}

第三题 翻转链表

点击跳转到力扣

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

双指针法

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null; // cur前一位
        ListNode cur = head;
        ListNode temp = null; // cur后一位
        // 后移并调换指向,交换后cur指向prev
        while (cur != null) {
            temp = cur.next;
            cur.next = prev;
            prev = cur;
            cur = temp;
        }
        return prev; // cur为null,prev为最后一位,即新的头结点
    }
}

递归法

class Solution {
    public ListNode reverseList(ListNode head) {
        return reverse(null, head);
    }

    private ListNode reverse(ListNode prev, ListNode cur) {
        if (cur == null) {
            return prev; // cur到了原本最后一位指向的空,返回最后一位即新的头指针tprev
        }
        ListNode temp = cur.next; // 保存cur的下一位
        cur.next = prev; // 指向变,cur指向prev
        return reverse(cur, temp); // 递归,cur和prev一起往后移动一位
    }
}

递归法(逆序)

class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null) 
            return null;
        if (head.next == null)
            return head;
        // 递归完成后,last一直等于尾节点
        ListNode last = reverseList(head.next);
        // 调换传入head和上一次递归的head的指向
        head.next.next = head; 
        // 不写这句代码,head和head.next会形成环
        head.next = null;
        return last; // 返回尾节点,即新的头结点
    }
}

class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null) 
            return null;
        if (head.next == null) 
            return head;
        Stack<ListNode> stack = new Stack<>();
        ListNode cur = head;
        // 入栈
        while (cur != null) {
            stack.push(cur);
            cur = cur.next;
        }
        // 创建一个虚拟头结点
        ListNode dumyhead = new ListNode(0);
        cur = dumyhead;
        // 出栈
        while (!stack.isEmpty()) {
            cur.next = stack.pop();
            cur = cur.next;
        } 
        // cur此时是尾节点,应该指向null
        cur.next = null;
        return dumyhead.next;
    }
}

第四题 两两交换链表中的节点

点击跳转到力扣

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

双指针法

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode swapPairs(ListNode head) {
        // 虚拟头结点
        ListNode dumyhead = new ListNode(-1);
        dumyhead.next = head;

        ListNode pre = dumyhead; // 指向交换的两节点的前一位
        ListNode temp; // 保存交换的两节点的后一位
        ListNode first; // 交换的前一位节点
        ListNode second; // 交换的后一位节点

        while (pre.next != null && pre.next.next != null) {
            first = pre.next;
            second = pre.next.next;
            temp = second.next;
            pre.next = second;
            second.next = first;
            first.next = temp;
            pre = first;
        }
        return dumyhead.next;
    }
}

递归法

class Solution {
    public ListNode swapPairs(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode next = head.next;
        //递归
        ListNode newNode = swapPairs(next.next);
        next.next = head; // 传入的后一位指向前一位
        head.next = newNode; // 传入的前一位指向上次递归的next
        // next即指向改变后,现在两节点的后一位
        return next;
    }
}

第五题 删除链表的倒数第N个节点

点击跳转到力扣

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

普通解法

使用链表的基本操作先算出链表长度size,再根据索引size - N删去该元素

快慢指针法

可画图理解

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        // 虚拟头结点,因为可能会删去头结点
        ListNode dumyhead = new ListNode(0);
        dumyhead.next = head;
        ListNode fast = dumyhead;
        ListNode slow = dumyhead;
        // 相差n个节点
        for (int i = 0; i <= n; i++) {
            fast = fast.next;
        }
        while (fast != null) {
            fast = fast.next;
            slow = slow.next;
        }
        // fast到null时,slow位于要删除节点的前一位
        slow.next = slow.next.next;
        return dumyhead.next;
    }
}

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        // 虚拟头结点,因为可能会删去头结点
        ListNode dumyhead = new ListNode(0, head);
        Stack<ListNode> stack = new Stack<>();
        ListNode cur = dumyhead;
        // 入栈
        while (cur != null) {
            stack.push(cur);
            cur = cur.next;
        } 
        // 出栈
        for (int i = 0; i < n; i++) {
            stack.pop();
        }
        ListNode pre = stack.peek();
        pre.next = pre.next.next;
        return dumyhead.next;
    }
}

第六题 链表相交

点击跳转力扣

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构 

双指针法一

比较两链表的后半段,即找到长链表和短链表,截取长链表使它和短链表长度相等,然后开始比较节点(注意:比较的是链表节点,而不是节点的值

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode curA = headA;
        ListNode curB = headB;
        int lenA = 0, lenB = 0;
        // 求出两链表长度
        while (curA != null) {
            lenA++;
            curA = curA.next;
        }
        while (curB != null) {
            lenB++;
            curB = curB.next;
        }
        // 长链表和短链表 (名字不能起short和long)
        // 特殊情况是两链表长度相等,必须加上等号
        ListNode longList = lenA >= lenB ? headA : headB;
        ListNode shortList = lenA < lenB ? headA : headB;
        int max = lenA >= lenB ? lenA : lenB;
        int min = lenA < lenB ? lenA : lenB;
        int gap = max - min; // 长度差
        while (gap-- > 0) {
            longList = longList.next;
        } // 长度相差几,长链表往后移动几位
        while (shortList != null) {
            if (longList == shortList) {
                return shortList;
            } 
            shortList = shortList.next;
            longList = longList.next;
        }
        return null;
    }
}

双指针法二

把两链表合并,在headA的后面加上headB,在headB的后面加上haedA。如果有相交点,那么总会相遇;如果没有相交点,那么遍历完都是null返回null。(可画图理解)

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) {
            return null;
        }
        ListNode pA = headA, pB = headB;
        while (pA != pB) {
            // 遍历完自己这边后,交换边遍历,使得如果有交点,则必定相遇
            pA = pA == null ? headB : pA.next;
            pB = pB == null ? headA : pB.next;
        }
        // 没交点,返回null
        return pA;
    }
}

哈希表

注意:Java中的Collection接口是单列集合的根接口,ListSet是它的两个子接口

  • List(列表):一个有序的集合(元素可以重复)。它的主要实现类有ArrayListLinkedList等。
  • Set(集合):一个不允许出现重复元素的集合。它的主要实现类有HashSetTreeSet等。

它们都实现了根接口的contains方法,但底层效率却不相同。

List底层使用equals方法遍历整个列表,时间效率为O(n)。

Set基于hash表实现,底层使用hashCode方法计算对象的哈希值,并以此确定对象的存储位置,时间效率接近O(1)

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        Set<ListNode> visited = new HashSet<>();
        ListNode temp = headA;
        // 将headA链表存入visited中
        while (temp != null) {
            visited.add(temp);
            temp = temp.next;
        }
        temp = headB;
        // 比较headB的各个节点headA是否包含
        while (temp != null) {
            if (visited.contains(temp)) {
                return temp;
            }
            temp = temp.next;
        }
        return null;
    }
}

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        // 提前排除,有备无患
        if (headA == null || headB == null) {
            return null;
        }

        ListNode curA = headA;
        ListNode curB = headB;
        Stack<ListNode> stackA = new Stack<>();
        Stack<ListNode> stackB = new Stack<>();
        while(curA != null) {
            stackA.push(curA);
            curA = curA.next;
        }
        while(curB != null) {
            stackB.push(curB);
            curB = curB.next;
        }
        // 记录弹出的节点,以便返回
        ListNode temp = null;
        while (!stackA.isEmpty() && !stackB.isEmpty()) {
            ListNode nodeA = stackA.pop();
            ListNode nodeB = stackB.pop();
            if (nodeA == nodeB) {
                temp = nodeB;
                continue;
            } else {
                // 若无相等节点,返回null
                // 若遇到不等节点,返回上个弹出的节点
                return temp;
            }
        }
        // 栈空且弹出的节点都相等,返回上个弹出节点
        return temp;
    }
}

第七题 环形链表

点击跳转力扣

给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,返回null。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。不允许修改 链表。

快慢指针法

(图片结合代码理解)

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        // 慢节点一次走一格,快捷点一次走两格
        // 因为fast相对于slow每次多走一格,可列方程
        ListNode slow = head;
        ListNode fast = head;

        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) { // 有环
                ListNode headNode = head; // 头结点
                ListNode meetingNode = slow; // 相遇节点
                while (headNode != meetingNode) {
                    headNode = headNode.next;
                    meetingNode = meetingNode.next;
                }
                return meetingNode;
            }
        }
        // 没有环
        return null;
    }
}

哈希表

public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode pos = head;
        // visited记录遇到的所有节点
        Set<ListNode> visited = new HashSet<>();
        while (pos != null) {
            // 一个节点遇到两次,即入口
            if (visited.contains(pos)) {
                return pos;
            } else {
                visited.add(pos);
                pos = pos.next;
            }
        }
        // 没环
        return null;
    }
}

  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值