【数据结构】LeetCode必刷题之链表

文章目录

1、002——两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

/**
 * 思路:从前往后遍历,求和,进位,用指针curr连接,最后记得判断temp进位是否为0,
 * 不为0需要进位
 */
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    ListNode head = new ListNode(0);
    ListNode h1 = l1, h2 = l2, curr = head;
    int temp = 0, num1, num2, sum;
    while (h1 != null || h2 != null) {
        num1 = h1 == null ? 0 : h1.val;
        num2 = h2 == null ? 0 : h2.val;
        sum = num1 + num2 + temp;
        temp = sum / 10;
        curr.next = new ListNode(sum % 10);
        curr = curr.next;
        if (h1 != null) {
            h1 = h1.next;
        }
        if (h2 != null) {
            h2 = h2.next;
        }
    }
    if (temp != 0) {
        curr.next = new ListNode(temp);
    }
    return head.next;
}

2、021——合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的

img

/**
 * 思路:双指针,分别指向两个链表,然后从左到右比较,如果哪个小,
 * 就把那个加到新链表中,同时它的指向后移,直到其中一个遍历完毕,
 * 把另一个链表剩余部分,加到新链表尾部
 */
public static ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    ListNode head = new ListNode(0);
    ListNode h1 = l1, h2 = l2, curr = head;
    while (h1 != null && h2 != null) {
        if (h1.val < h2.val) {
            curr.next = new ListNode(h1.val);
            h1 = h1.next;
        } else {
            curr.next = new ListNode(h2.val);
            h2 = h2.next;
        }
        curr = curr.next;
    }
    if (h1 == null) {
        curr.next = h2;
    }
    if (h2 == null) {
        curr.next = h1;
    }
    return head.next;
}


/**
 * 思路: 递归,先确定递归终止条件,当l1为null,返回l2,当l2为null返回l1
 * 每一层都返回排好序的链表头
 * 如果l1小,就把l1.next指向排好序的表头
 */
public static ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    if (l1 == null) {
        return l2;
    }
    if (l2 == null) {
        return l1;
    }
    if (l1.val < l2.val) {
        l1.next = mergeTwoLists(l1.next, l2);
        return l1;
    } else {
        l2.next = mergeTwoLists(l1, l2.next);
        return l2;
    }
}

3、024——两两交换链表中的节点

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

/**
 * 递归法
 * 1、返回值
 * 2、调用单元做了什么
 * 3、终止条件
 *
 * 在本题中
 * 1、返回值:交换完成的子链表
 * 2、调用单元:设需要交换的两个节点为head和next,head连接后面交换完成的子链表,next
 *             连接head,完成交换
 * 3、终止条件:head为null或者next为null
 */
public ListNode swapPairs1(ListNode head) {
    if (null == head || null == head.next) {
        return head;
    }
    ListNode next = head.next;
    head.next = swapPairs1(next.next);
    next.next = head;
    return next;
}


/**
 * 非递归
 */
public ListNode swapPairs2(ListNode head) {
    ListNode pre = new ListNode(0);
    pre.next = head;
    ListNode temp = pre;
    while (temp.next != null && temp.next.next != null) {
        // 第一个节点
        ListNode start = temp.next;
        // 第二个节点
        ListNode end = temp.next.next;
        // 把上一组第二个节点的next指向新一组交换后的第一个节点
        temp.next = end;
        // 前一个的next,指向后一个的next(保证链表不死循环)
        start.next = end.next;
        // 交换 把后一个节点指向前一个
        end.next = start;
        // 更新temp值
        temp = start;
    }
    return pre.next;
}

4、025—— K 个一组翻转链表

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。

k 是一个正整数,它的值小于或等于链表的长度。

如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

进阶:

你可以设计一个只使用常数额外空间的算法来解决此问题吗?
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

img

/**
 * 思路:遍历k个节点,找到k个节点的首尾,然后反转,将pre节点指向反转后的头结点
 * 将反转后的节点指向待反转部分的头结点,然后更新pre和end的位置
 */
public ListNode reverseKGroup(ListNode head, int k) {
    ListNode dummy = new ListNode(-1);
    dummy.next = head;

    ListNode pre = dummy;
    ListNode end = dummy;
    while (end.next != null) {
        for (int i = 0; i < k && end != null; i++) {
            end = end.next;
        }
        if (end == null) {
            break;
        }
        ListNode start = pre.next;
        ListNode next = end.next;
        end.next = null;
        pre.next = reverseList(start);
        start.next = next;
        pre = start;
        end = pre;
    }
    return dummy.next;
}

/**
* 反转链表
*/
private ListNode reverseList(ListNode head) {
    //单链表为空或只有一个节点,直接返回原单链表
    if (head == null || head.next == null) {
        return head;
    }
    //前一个节点指针
    ListNode preNode = null;
    //当前节点指针
    ListNode curNode = head;
    while (curNode != null) {
        ListNode temp = curNode.next;
        curNode.next = preNode;
        preNode = curNode;
        curNode = temp;
    }
    return preNode;
}

5、061——旋转链表

给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。

img

/**
 * 思路: 遍历链表,确定链表长度len,同时找到队尾,如果k为的整数倍,直接返回无需移动
 * 如果不是,找到len - 1 - (k % len)位置的前一个元素,也就是新的队尾,将队尾next置null
 * 将旧队尾指向旧表头,返回新表头
 *
 * 为什么len - 1 - (k % len)的下标就是新的表头呢?
 * 因为由题可知,旋转完的数据的新表头就是原链表倒数第k个元素,
 * 也就是len - 1 - (k % len)
 */
public static ListNode rotateRight(ListNode head, int k) {
    if (head == null || head.next == null || k == 0) {
        return head;
    }
    ListNode curr = head;
    ListNode tail = head;
    int len = 0;
    while (curr != null) {
        len++;
        if (curr.next == null) {
            tail = curr;
        }
        curr = curr.next;
    }
    if (k % len == 0) {
        return head;
    }
    ListNode oldHead = head;
    ListNode newTail = head;
    int index = len - (k % len) - 1;
    int i = 0;
    while (i++ < index) {
        newTail = newTail.next;
    }
    ListNode newHead = newTail.next;
    newTail.next = null;
    tail.next = oldHead;
    return newHead;
}

6、076——最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""

public String minWindow(String s, String t) {
    char[] chars = s.toCharArray(), chart = t.toCharArray();
    int n = chars.length, m = chart.length;

    int[] hash = new int[128];
    for (char ch : chart) hash[ch]--;

    String res = "";
    for (int i = 0, j = 0, cnt = 0; i < n; i++) {
        hash[chars[i]]++;
        if (hash[chars[i]] <= 0) cnt++;
        while (cnt == m && hash[chars[j]] > 0) hash[chars[j++]]--;
        if (cnt == m)
            if (res.equals("") || res.length() > i - j + 1)
                res = s.substring(j, i + 1);
    }
    return res;
}

7、082——删除排序链表中的重复元素 II

给定一个已排序的链表的头 head删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表

/**
 * 思路:判断当前元素和其后面的元素值是否相同,不相同向后遍历,
 * 相同的话,while遍历找到与当前元素不相同的元素
 */
public ListNode deleteDuplicates(ListNode head) {
    ListNode pre = new ListNode(0);
    pre.next = head;
    ListNode curr = pre;
    while (curr.next != null && curr.next.next != null) {
        if (curr.next.val == curr.next.next.val) {
            // 记录第一个重复节点的位置
            ListNode temp = curr.next;
            // while循环把之后相同元素全移除
            while (temp.next != null && temp.val == temp.next.val) {
                temp = temp.next;
            }
            curr.next = temp.next;
        } else {
            curr = curr.next;
        }
    }
    return pre.next;
}

8、083—— 删除排序链表中的重复元素

给定一个已排序的链表的头 head删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表

/**
 * 思路: 比较当前元素和他下一个元素的值,如果不相等继续向后遍历
 * 如果相等就把当前元素的指针指向它下一个的后一个元素,继续比较
 */
public ListNode deleteDuplicates(ListNode head) {
    ListNode curr = head;
    while (curr != null && curr.next != null) {
        if (curr.val == curr.next.val) {
            curr.next = curr.next.next;
        } else {
            curr = curr.next;
        }
    }
    return head;
}

9、086——分隔链表

给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。

你应当 保留 两个分区中每个节点的初始相对位置。

/**
 * 思路:构造两个头结点,一个用来存放比目标数小的节点,另一个存放大于等于
 * 目标值的节点,然后再将两个连起来
 */
public static ListNode partition(ListNode head, int x) {
    ListNode small = new ListNode(0);
    ListNode big = new ListNode(0);
    ListNode smallCurr = small;
    ListNode bigCurr = big;
    ListNode curr = head;
    while (curr != null) {
        if (curr.val < x) {
            smallCurr.next = curr;
            smallCurr = smallCurr.next;
        } else {
            bigCurr.next = curr;
            bigCurr = bigCurr.next;
        }
        curr = curr.next;
    }
    smallCurr.next = big.next;
    bigCurr.next = null;
    return small.next;
}

10、092——反转链表 II

给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

来源:力扣(LeetCode)

/**
 * 思路:使用栈将需要旋转区间的元素存起来,然后才拿出来,后进先出,倒序
 */
public ListNode reverseBetween(ListNode head, int left, int right) {
    Stack<ListNode> stack = new Stack<>();
    ListNode before = new ListNode(-1);
    ListNode after = new ListNode(-1);
    ListNode curr = head, currBefore = before, currAfter = after;
    int count = 0;
    while (curr != null) {
        count++;
        if (count < left) {
            currBefore.next = curr;
            currBefore = currBefore.next;
        } else if (count >= left && count <= right) {
            stack.push(curr);
        } else {
            currAfter.next = curr;
            currAfter = currAfter.next;
        }
        curr = curr.next;
    }

    while (!stack.empty()) {
        currBefore.next = stack.pop();
        currBefore = currBefore.next;
    }
    currBefore.next = after.next;
    currAfter.next = null;
    return before.next;
}

/**
 * 双指针:找到需要旋转的区间左边元素及其前一个元素,可以将其之后的元素采用头插法,
 * 插到其前一个元素的后面,正好是旋转
 */
public ListNode reverseBetween1(ListNode head, int left, int right) {
    ListNode pre = new ListNode(-1);
    pre.next = head;

    ListNode g = pre;
    ListNode p = head;
    // 找到左边的前一个节点位置
    for (int i = 0; i < left - 1; i++) {
        g = g.next;
        p = p.next;
    }
    // 头插法插入处于旋转区间的元素
    for (int i = 0; i < right - left; i++) {
        ListNode remove = p.next;
        p.next = p.next.next;
        remove.next = g.next;
        g.next = remove;
    }
    return pre.next;
}

11、138——复制带随机指针的链表

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。

例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

返回复制链表的头节点。

/**
 * 思路:使用map,遍历链表同时new一个相同值的对象,
 * 第二次遍历复制,因为map中存在的key,value是一样的即
 * map.get(node).next = map.get(node.next);
 * map.get(node).random = map.get(node.random);
 */
public Node copyRandomList(Node head) {
    if (null == head) {
        return null;
    }
    Map<Node, Node> map = new HashMap<>();
    Node node = head;
    while (node != null) {
        map.put(node, new Node(node.val));
        node = node.next;
    }
    node = head;
    while (node != null) {
        map.get(node).next = map.get(node.next);
        map.get(node).random = map.get(node.random);
        node = node.next;
    }
    return map.get(head);
}

12、143——重排链表

给定一个单链表 L 的头节点 head ,单链表 L 表示为:

L0 → L1 → … → Ln - 1 → Ln
请将其重新排列后变为:

L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …
不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

img

//使用有序集合
public void reorderList(ListNode head) {
    if (null == head) {
        return;
    }
    // 存到有序集合中
    List<ListNode> list = new ArrayList<>();
    ListNode curr = head;
    while (curr != null) {
        list.add(curr);
        curr = curr.next;
    }
    int l = 0, r = list.size() - 1;
    while (l < r) {
        list.get(l).next = list.get(r);
        l++;
        //偶数个节点会提前相遇
        if (l == r) {
            break;
        }
        list.get(r).next = list.get(l);
        r--;
    }
    list.get(l).next = null;
}


/**
* 快慢指针 分割中点
*/
public void reorderList2(ListNode head) {
    if (head == null || head.next == null || head.next.next == null) {
        return;
    }
    //找中点,链表分成两个
    ListNode slow = head;
    ListNode fast = head;
    while (fast.next != null && fast.next.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }

    ListNode newHead = slow.next;
    slow.next = null;

    //第二个链表倒置
    newHead = reverseList(newHead);

    //链表节点依次连接
    while (newHead != null) {
        ListNode temp = newHead.next;
        newHead.next = head.next;
        head.next = newHead;
        head = newHead.next;
        newHead = temp;
    }

}

private ListNode reverseList(ListNode head) {
    if (head == null) {
        return null;
    }
    ListNode tail = head;
    head = head.next;

    tail.next = null;

    while (head != null) {
        ListNode temp = head.next;
        head.next = tail;
        tail = head;
        head = temp;
    }
    return tail;
}

13、147——对链表进行插入排序

给定单个链表的头 head ,使用 插入排序 对链表进行排序,并返回 排序后链表的头 。

插入排序 算法的步骤:

插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。
每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。
重复直到所有输入数据插入完为止。
下面是插入排序算法的一个图形示例。部分排序的列表(黑色)最初只包含列表中的第一个元素。每次迭代时,从输入数据中删除一个元素(红色),并就地插入已排序的列表中。

对链表进行插入排序。

img

/**
 * 思路:按顺序遍历,如果后面元素的值大于等于前一个元素,继续向后遍历,
 * 否则删除这个元素,从新从头遍历 找到他合适的插入位置,从新插入进来
 */
public static ListNode insertionSortList1(ListNode head) {
    ListNode pre = new ListNode(-1);
    pre.next = head;
    ListNode curr = head;

    ListNode preCurr,temp;
    while (curr != null && curr.next != null) {
        if (curr.val <= curr.next.val) {
            curr = curr.next;
        } else {
            // 保存并删除当前元素的下一个节点
            temp = curr.next;
            curr.next = curr.next.next;
            preCurr = pre;
            while (preCurr.next != null) {
                if (preCurr.next.val <= temp.val) {
                    preCurr = preCurr.next;
                } else {
                    //找到插入的位置
                    temp.next = preCurr.next;
                    preCurr.next = temp;
                    break;
                }
            }
        }
    }
    return pre.next;
}

14、148——排序链表

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

public ListNode sortList(ListNode head) {
    //当链表的为空或链表只有一个元素的时候,直接返回。
    if (head == null || head.next == null) {
        return head;
    }
    //定义快慢指针,来找出链表中的中间节点
    ListNode fast = head;
    ListNode slow = head;
    //这里的判断条件是;倒数第一个和倒数第二个节点
    //在为偶数个的时候,会取第一个数作为第一个中间数
    //这里特别要注意;fast !=null && fast.next.next != null 者两个条件的区别
    while (fast.next != null && fast.next.next != null) {
        //快指针移动两步,慢指针移动一步
        fast = fast.next.next;
        slow = slow.next;
    }
    //定义一个伪节点,来指向被第一个对半拆分后的,第二个链表的头节点
    ListNode temp = slow.next;
    //对半拆分链表,
    slow.next = null;
    //左拆分,
    ListNode left = sortList(head);
    //右拆分
    ListNode right = sortList(temp);
    //定义一个返回结果的伪节点的指针
    ListNode dumpy = new ListNode(0);
    //定义一个用于遍历链表的伪节点
    ListNode prev = dumpy;
    //当左链表和右链表不为空时,几句比较两个链表节点的值
    while (left != null && right != null) {
        //左链表的节点的值小于右链表的节点值时,就拼接左链表的节点
        //并将左链表的节点的指针后移一位
        if (left.val <= right.val) {
            prev.next = left;
            left = left.next;
        } else {
            //否则就,拼接右链表的节点,移动右链表的节点的指针
            prev.next = right;
            right = right.next;
        }
        //拼接了一个节点后,就移动伪节点的指针
        prev = prev.next;
    }
    //如果左链表有剩余元素,就拼接左链表,否则就拼接右链表
    prev.next = left != null ? left : right;
    //返回伪节点的指向的头节点
    return dumpy.next;
}

15、160——相交链表

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

图示两个链表在节点 c1 开始相交:

img

/**
 * 思路:已知不存在环,可以直接遍历比较,如果其中一个遍历完则将其指向另一个链表的头结点
 * 如果存在交点正好可以遍历到或者两者同时遍历完
 */
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    if(headA == null || headB == null){
        return null;
    }
    ListNode A = headA;
    ListNode B = headB;
    while(A != B){
        A = A == null ? headB : A.next;
        B = B == null ? headA : B.next;
    }
    return A;
}

16、203——移除链表元素

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

img

/**
 * 思路:遍历,找到值相同的就将前一个元素指向他的下一个节点
 */
public ListNode removeElements(ListNode head, int val) {
    if (head == null) {
        return null;
    }
    ListNode pre = new ListNode(-1);
    pre.next = head;
    ListNode curr = head;
    ListNode currPre = pre;
    while (curr != null) {
        if (curr.val == val) {
            currPre.next = curr.next;
        } else {
            currPre.next = curr;
            currPre = currPre.next;
        }
        curr = curr.next;
    }
    return pre.next;
}

17、206——反转链表

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

/**
 * 递归法
 * 1、返回值
 * 2、调用者做了什么
 * 3、终止条件
 * 本题
 * 1、返回反转后的链表表头
 * 2、调用者将他本身指向null,将他后继节点指向自己
 * 3、终止条件找到链表尾部
 */
public ListNode reverseList(ListNode head) {
    if (head == null || head.next == null) {
        return head;
    }
    ListNode ans = reverseList(head.next);
    head.next.next = head;
    head.next = null;
    return ans;
}

/**
 * 双指针
 */
public ListNode reverseList(ListNode head) {
    ListNode pre = head;
    ListNode ans = null;
    while (pre != null) {
        ListNode temp = pre.next;
        pre.next = ans;
        ans = pre;
        pre = temp;
    }
    return ans;
}

18、234——回文链表

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false

/**
 * 思路:快慢指针,找到中间节点,然后分割,将后半部分反转,再与前面比较
 */
public static boolean isPalindrome(ListNode head) {
    ListNode pre = head;
    ListNode slow = head;
    ListNode fast = head;
    while (fast.next != null && fast.next.next != null) {
        fast = fast.next.next;
        slow = slow.next;
    }
    ListNode newHead = reverseList(slow.next);
    while (newHead != null) {
        if (newHead.val != pre.val) {
            return false;
        }
        newHead = newHead.next;
        pre = pre.next;
    }
    return newHead == null;
}

/**
 * 反转链表并返回反转后的新表头
 */
private static ListNode reverseList(ListNode head) {
    ListNode pre = head;
    ListNode ans = null;
    while (pre != null) {
        ListNode temp = pre.next;
        pre.next = ans;
        ans = pre;
        pre = temp;
    }
    return ans;
}

19、237——删除链表中的节点

请编写一个函数,用于 删除单链表中某个特定节点 。在设计函数时需要注意,你无法访问链表的头节点 head ,只能直接访问 要被删除的节点 。

题目数据保证需要删除的节点 不是末尾节点 。

/**
 * 思路:将下一个元素的节点值给到当前节点,把下一个节点删除
 */
public void deleteNode(ListNode node) {
    node.val = node.next.val;
    node.next = node.next.next;
}

20、328——奇偶链表

给定单链表的头节点 head ,将所有索引为奇数的节点和索引为偶数的节点分别组合在一起,然后返回重新排序的列表。

第一个节点的索引被认为是 奇数 , 第二个节点的索引为 偶数 ,以此类推。

请注意,偶数组和奇数组内部的相对顺序应该与输入时保持一致。

你必须在 O(1) 的额外空间复杂度和 O(n) 的时间复杂度下解决这个问题。

/**
 * 思路:遍历,把奇数节点放一起,偶数放一起,再连接起来
 */
public static ListNode oddEvenList1(ListNode head) {
    if (head == null) {
        return head;
    }
    ListNode evenHead = head.next;
    ListNode odd = head, even = evenHead;
    while (even != null && even.next != null) {
        odd.next = even.next;
        odd = odd.next;
        even.next = odd.next;
        even = even.next;
    }
    odd.next = evenHead;
    return head;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值