面试+算法之链表(Java):链表公共节点,判断是否有环,升序排序,删除排序后重复元素,合并有序链表,K个一组翻转链表,分隔链表,有序链表转换二叉搜索树,判断单链表是否为回文链表(Java)

概述

链表作为面试必问的数据结构之一,掌握关于链表的基础算法很有必要。

链表的定义:

public class ListNode {
	private ListNode next;
	private int val;

	ListNode(int val) {
		this.val = val;
	}
}

Java语言里,链表ListNode并不是一个简单的结构,为啥可以使用==来判断?

在Java中,==运算符用于比较两个引用是否指向同一个对象,检查的是引用本身,而不是引用指向的对象的内容。ListNode通常是通过引用来进行操作。使用==运算符判断两个链表节点是否相等,是为了判断它们是否指向同一个内存地址(即它们是否是同一个对象),而不是判断它们的值是否相等。

二分查找

链表能不能使用二分查找?

不能。二分查找是一种高效的查找算法,适用于已排序且静态的数组或列表,而链表不支持随机内存访问,即每一个节点的地址不能在O(1)的时间复杂度内获得。

判断单向链表是否有环

经典题,解决思路:

  1. 取数链表的元素值,存入到额外的数据结构,如HashSet。从head节点出发,如果节点的值不在HashSet里面,则存入;继续移动节点,如果节点数值存在于HashSet里,则表明有环。
  2. 设置两个指针,都从链表头节点出发,一个每次向后移动一步,另一个移动两步,速度不一样,如果存在环,则一定会相遇:
/**
 * 判断给定链表是否有环
 */
public static boolean hasLoop(ListNode head) {
    if (head == null || head.next == null) {
        return false;
    }
    ListNode slow = head.next;
    ListNode fast = head.next.next;
    while (true) {
        if (fast == null || fast.next == null) {
            // fast走到链表尾
            return false;
        } else if (fast.next == slow || fast == slow) {
            return true;
        } else {
            slow = slow.next;
            fast = fast.next.next;
        }
    }
}

引申

  1. 找到环的入口节点
    fast每次走两步,slow每次走一步,slow和fast能重合,则确定单向链表有环路。接下来,让fast回到链表头部,每次步长为1,则当slow和fast再次相遇时,就是环路的入口。

输入两个链表,找出第一个公共结点

  1. 链表是否为空
  2. 链表是否是无环链表?是否是单(向)链表?
  3. 如果两个链表存在公共结点,那从公共结点开始一直到链表的结尾都是一样的,因此只需要从链表的结尾开始,往前搜索,找到最后一个相同的结点即可。单向链表,只能从前向后搜索,借助栈来完成。先把两个链表依次装到两个栈中,然后比较两个栈的栈顶结点是否相同,如果相同则出栈,如果不同,那最后相同的结点就是公共节点。
  4. 先求2个链表的长度,让长的先走两个链表的长度差,然后再一起走,直到找到第一个公共结点。
  5. 由于2个链表都没有环,可以把第二个链表接在第一个链表后面,这样就把问题转化为求环的入口节点问题。
  6. 两个指针p1和p2分别指向链表A和链表B,它们同时向前走,当走到尾节点时,转向另一个链表,比如p1走到链表A的尾节点时,下一步就走到链表B,p2走到链表B的尾节点时,下一步就走到链表A,当p1==p2时,就是链表的相交点
/**
 * 两个链表的第一个公共结点
 */
public static ListNode findFirstCommonNode(ListNode current1, ListNode current2) {
    HashMap<ListNode, Integer> hashMap = new HashMap<>(16);
    while (current1 != null) {
        hashMap.put(current1, null);
        current1 = current1.next;
    }
    while (current2 != null) {
        if (hashMap.containsKey(current2)) {
            return current2;
        } else if (getNoLoopLength(current2) == 1) {
            // 链表2长度为1的特殊情况
            return null;
        }
        current2 = current2.next;
    }
    return null;
}

/**
 * 无环单链表长度
 */
private static int getNoLoopLength(ListNode head) {
    int length = 0;
    ListNode current = head;
    while (current != null) {
        length++;
        current = current.next;
    }
    return length;
}

链表翻转

/**
 * 链表翻转
 */
public static ListNode reverse(ListNode head) {
    ListNode prev = null;
    ListNode cur = head;
    while (cur != null) {
        ListNode next = cur.next; // 临时保存下一个节点
        cur.next = prev; // 反转当前节点的指针
        prev = cur; // 移动prev指针
        cur= next; // 移动cur指针
    }
    return prev; // prev最终指向反转后的新头结点
}

链表旋转

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

/**
 * 给定链表头节点head,旋转链表,将链表每个节点向右移动k个位置
 */
public static ListNode rotateRight(ListNode head, int k) {
    if (head == null) {
        return null;
    }
    int len = 0;
    ListNode tmp = head;
    while (tmp != null) {
        tmp = tmp.next;
        len++;
    }
    // 判断链表长度和k的关系
    k = k % len;
    ListNode fast = head;
    ListNode slow = head;
    // 两指针相差k(修正后)节点
    for (int i = 0; i < k; i++) {
        fast = fast.next;
    }
    while (fast.next != null) {
        fast = fast.next;
        slow = slow.next;
    }
    // fast指向最后一个节点,和原链表的头节点连接起来
    fast.next = head;
    // slow指向节点的下一个节点是倒数第k个节点,即旋转后的头节点
    ListNode newHead = slow.next;
    // 断开链接避免成环
    slow.next = null;
    return newHead;
}

求链表倒数第k个节点

给定一个单向链表,输出该链表中倒数第k个节点,链表的倒数第0个节点为链表的尾指针。

分析:设置两个指针fast、slow都指向head,fast向前走k步,这样fast和slow之间就间隔k个节点,fast和slow同时向前移动,直至slow走到链表末尾。

/**
 * 求链表倒数第k个节点
 */
public static ListNode getKthFromEnd(ListNode head, int k) {
    if (head == null || k <= 0) {
        return null;
    }
    ListNode fast = head;
    ListNode slow = head;
    // 让快指针先移动k步,如果链表长度小于k,返回null
    for (int i = 0; i < k; i++) {
        if (fast == null) {
            return null;
        }
        fast = fast.next;
    }
    // 快慢指针一起移动,直到快指针到达链表末尾
    while (fast != null) {
        fast = fast.next;
        slow = slow.next;
    }
    // 返回慢指针所指的节点
    return slow;
}

求链表中间节点

求链表的中间节点,如果链表的长度为偶数,返回中间两个节点的任意一个,若为奇数,则返回中间节点。

分析:可以先求链表的长度,然后计算出中间节点所在链表顺序的位置。但如果只能扫描一遍链表?最高效的解法,通过两个指针来完成。用两个指针从链表头节点开始,一个指针每次向后移动两步,一个每次移动一步,直到快指针移到到尾节点,那么慢指针即是所求。

/**
 * 求链表中间节点
 */
public static ListNode middleNode(ListNode head) {
    if (head == null) {
        return null;
    }
    ListNode slow = head;
    ListNode fast = head;
    // 快指针每次移动两步,慢指针每次移动一步
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    // 返回慢指针所指的节点,即为链表的中间节点
    return slow;
}

删除链表元素

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

public static ListNode removeElements(ListNode head, int val) {
    ListNode top = new ListNode(0);
    top.next = head;
    ListNode pre = top;
    ListNode temp = head;
    while (temp != null) {
        if (temp.val == val) {
            pre.next = temp.next;
        } else {
            pre = temp;
        }
        temp = temp.next;
    }
    return top.next;
}

合并两个有序链表

初始化一个空的链表,遍历两个链表直到其中一个为空,然后将剩余的节点接到新链表的末尾。

/**
 * 合并两个有序链表并保持输出链表有序
 */
public static ListNode merge(ListNode l1, ListNode l2) {
    ListNode tmp = new ListNode();
    ListNode cur = tmp;
    // 遍历两个链表,直到其中一个链表为空
    while (l1 != null && l2 != null) {
        if (l1.val <= l2.val) {
            cur.next = l1;
            l1 = l1.next;
        } else {
            cur.next = l2;
            l2 = l2.next;
        }
        cur= cur.next;
    }
    // 将剩余节点接到新链表末尾
    if (l1 != null) {
        cur.next = l1;
    } else {
        cur.next = l2;
    }
    // 返回新链表头节点
    return tmp.next;
}

升序排序

可使用归并排序算法,合并方法参考上面。

/**
 * 归并排序实现链表的排序:将链表递归地拆分成两个子链表,分别对其排序,然后将排序后的子链表合并成一个有序链表
 */
public static ListNode sortList(ListNode head) {
    if (head == null || head.next == null) {
        return head;
    }
    // 使用快慢指针找到链表的中间点
    ListNode slow = head;
    ListNode fast = head;
    ListNode prev = null;
    while (fast != null && fast.next != null) {
        prev = slow;
        slow = slow.next;
        fast = fast.next.next;
    }
    // 断开链表
    prev.next = null;
    // 递归排序左右部分
    ListNode l1 = sortList(head);
    ListNode l2 = sortList(slow);
    // 合并两个有序链表
    return merge(l1, l2);
}

删除排序链表中重复元素

/**
 * 删除排序链表中重复元素
 */
public static ListNode deleteDuplicates1(ListNode head) {
    if (head == null) {
        return null;
    }
    ListNode current = head;
    // 遍历链表
    while (current.next != null) {
        // 如果当前节点的值等于下一个节点的值
        if (current.val == current.next.val) {
            // 跳过下一个节点
            current.next = current.next.next;
        } else {
            // 移动到下一个节点
            current = current.next;
        }
    }
    return head;
}

K个一组翻转链表

给定一个链表,每k个节点一组进行翻转,返回翻转后的链表。k为小于或等于链表长度的正整数。如果节点总数不是k的整数倍,最后剩余的节点保持原有顺序。

public static ListNode reverseKGroup(ListNode head, int k) {
    if (head == null) {
        return null;
    }
    ListNode b = head;
    for (int i = 0; i < k; i++) {
        if (b == null) {
            return head;
        }
        b = b.next;
    }
    ListNode newHead = reverse(head, b);
    head.next = reverseKGroup(b, k);
    return newHead;
}

private static ListNode reverse(ListNode a, ListNode b) {
    ListNode pre, cur, nxt;
    pre = null;
    cur = a;
    nxt = a;
    while (nxt != b) {
        nxt = cur.next;
        cur.next = pre;
        pre = cur;
        cur = nxt;
    }
    return pre;
}

分隔链表

给定一个链表头节点head和一个特定值val,把所有小于val的节点都放置在大于或等于val的节点之前,保留两个分区中每个节点的初始相对位置。

public static ListNode partition(ListNode head, int x) {
    ListNode tmpHead1 = new ListNode(0);
    ListNode tmpHead2 = new ListNode(0);
    ListNode node1 = tmpHead1;
    ListNode node2 = tmpHead2;
    while (head != null) {
        if (head.val < x) {
            node1.next = head;
            head = head.next;
            node1 = node1.next;
            node1.next = null;
        } else {
            node2.next = head;
            head = head.next;
            node2 = node2.next;
            node2.next = null;
        }
    }
    node1.next = tmpHead2.next;
    return tmpHead1.next;
}

有序链表转换二叉搜索树

给定一个升序单链表,将其转换为高度平衡的二叉搜索树,即二叉树每个节点的左右两个子树的高度差的绝对值不超过1。注意:满足条件的二叉搜索树不止一个。

public static TreeNode sortedListToBST(ListNode head) {
    if (head == null) {
        return null;
    }
    return helper(head, null);
}

private static TreeNode helper(ListNode start, ListNode end) {
    if (start == end) {
        return null;
    }
    ListNode slow = start;
    ListNode fast = start;
    while (fast != end && fast.next != end) {
        slow = slow.next;
        fast = fast.next.next;
    }
    TreeNode root = new TreeNode(slow.val);
    root.left = helper(start, slow);
    root.right = helper(slow.next, end);
    return root;
}

判断单链表是否为回文链表

回文链表的定义:从head出发和从tail(反向)出发遍历,得到的数值一样。

例子:
1->2->1,输出:true
1->2->2->1,输出:true。
1->2->3,输出:false。

public static boolean isPalindrome(ListNode head) {
    if (head == null || head.next == null) {
        return true;
    }
    ListNode slow = head;
    ListNode quick = head;
    while (quick != null && quick.next != null) {
        quick = quick.next.next;
        slow = slow.next;
    }
    ListNode pre = null;
    ListNode p = slow;
    while (p != null) {
        ListNode temp = p.next;
        p.next = pre;
        pre = p;
        p = temp;
    }
    while (pre != null) {
        if (pre.val == head.val) {
            pre = pre.next;
            head = head.next;
        } else {
            return false;
        }
    }
    return true;
}

拓展

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

johnny233

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值