CodeTop刷题记录-京东(1)

  1. 环形链表 II 206. 反转链表 912. 排序数组

大家好,文章题目是codeTop上的热门题目,本文章是京东面试题目热门题目。虽然算法在后端日常开发中应用比较少,但是它对思维的开发是很有益的,也是进中、大厂的必备的东西。文章致力于用最简单的语言阐述解题思路和解题方法,欢迎大家讨论更优秀的解题方案。

ps:对题目的讲解分为三部分,题意描述、思路描述和代码编写。众所周知有思路不一定能编写出代码,思路的完整性有助于一次AC。

强烈建议参考代码一起思考,有些东西用语言表达不出来,也就是脑子会了,手不会。题解地址

题目

142. 环形链表 II

题意:

给出一个链表,链表包含本身数值和下一个数据的地址。需要判断出此链表存不存在环形结构,返回入环的节点。

思路:

若链表有环,那么循环链表的话会永远找不到出口,可以利用这个特性,就像人在操场跑步,只要第一名足够快,第一名和最后一名一定会相遇,在链表中循环也是一样利用套圈会相遇的特点,使用两个循环若相遇(相同值)就是有环,否则无环。

但是有个问题,虽然能相遇,但是不一定是在开始循环的节点相遇,还需要计算出入环节点。

这个时候就需要公式推导了(为什么公式可以推导?因为这个是有规律的)
我们称快的为fast,慢的为slow。为了好计算,fast的速度是2,slow为1,即fast一次循环两个节点,slow为一个。

  • 以路程为视角,S(fast) = 2S(slow)
  • 以fast追slow为视角,追了n圈才相遇,S(fast)=S(slow) + n * S(环长度)
  • 得出S(slow) = n * S(环长度)
  • 以slow为视角,S(slow) = res + m * S(环长度)

res为从起点到入环的距离,即slow再走res步就是答案。换个说法,从head走res步也是入环节点,那么从head和slow位置同时走,走res步则会相遇,只要判断相遇,相遇的点就是入环点,就是答案。

代码解析:

有了思路,剩下的是转换为代码,参考

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;


public class Main {

    public static void main(String[] args) {
        ListNode listNode1 = new ListNode(3);
        ListNode listNode2 = new ListNode(2);
        ListNode listNode3 = new ListNode(0);
        ListNode listNode4 = new ListNode(-4);
        listNode1.next = listNode2;
        listNode2.next = listNode3;
        listNode3.next = listNode4;
        listNode4.next = listNode2;

        ListNode res1 = detectCycle1(listNode1);
        System.out.println(res1.val);

        ListNode res2 = detectCycle2(listNode1);
        System.out.println(res2.val);
    }

    /**
     * 哈希法判断
     * 利用哈希表进行判断是是否存在此节点来判断是否存在环形
     * 每次循环时,首先判断是否在Set中
     * - 若不存在则加入到Set中
     * - 若存在则返回节点
     * - 若节点为空,返回空
     * <p>
     * 复杂度分析:O(N) O(N)
     *
     * @param head 链表
     * @return 结果
     */
    public static ListNode detectCycle1(ListNode head) {
        Set<ListNode> nodeSet = new HashSet<>();
        while (true) {
            if (head == null) {
                return null;
            }
            if (nodeSet.contains(head)) {
                return head;
            }
            nodeSet.add(head);
            head = head.next;
        }
    }

    /**
     * 双指针
     * AB两指针分别一次走2和1的距离,因为有距离差,若存在环形结构,一定会被套圈追上
     * 首先计算出相遇的位置,这个位置可以用公式表示(起点到环形距离为lenA,环形长度为lenB):
     * ∵ A走的速度快
     * ∴ Sa = 2 * Sb(A走过的距离=2*B走过的距离)
     * ∵ A走的多,在追B时,多走了很多圈
     * ∴ Sa = Sb + n * lenB
     * 整合后
     * ∴ Sb = n * lenB   Sa = 2 * n * lenB
     * 所以AB走的距离都可以用lenB表示。
     * <p>
     * 若从起点出发,最终停留在循环入口处,期间转了m圈用公式表示:
     * Res = lenA + m * lenB ①
     * ∵ n m 都是基于链表的实际数字,圈数与位置没有关系
     * ∵ Sb停留的位置一定在环上,但不一定在起点处,结合上面公式①
     * ∴ Sb + lenA 的位置一定是起点,也就是结果
     * <p>
     * 当B走lenA后,即为答案,问题转换为求lenA
     * Sb = n * lenB
     * 令Sa = 0
     * 当Sa = lenA时,Sb = lenA + n * lenB
     *
     * @param head 链表
     * @return 结果
     */
    public static ListNode detectCycle2(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while (true) {
            // 无环退出
            if (fast == null || fast.next == null) {
                return null;
            }
            // 走一次两个距离
            fast = fast.next.next;
            // 走一次一个距离
            slow = slow.next;
            // 相遇退出
            if (fast == slow) {
                break;
            }
        }
        // 最后一步,相遇即可
        fast = head;
        while (fast != slow) {
            fast = fast.next;
            slow = slow.next;
        }
        return fast;
    }

    static class ListNode {
        int val;
        ListNode next;

        ListNode(int x) {
            val = x;
            next = null;
        }
    }
}

206. 反转链表

题意:

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

思路:

既然是链表,进行循环即可,因为需要反向输出,循环的第一个节点就是最后一个节点,每次循环时,新建一个节点,使此节点的next指针指向循环节点的值。

题外话:程序是一个呆板的、写死的、只能按照预先设置好的执行,所以思考时需要往通用型方向思考,特殊情况特殊判断,即可完成一个健壮性程序。

代码解析:
public class ReverseList {

    /**
     * 题意:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
     * 思路:既然是链表,进行循环即可,因为需要反向输出,循环的第一个节点就是最后一个节点,每次循环时,新建一个节点,使此节点的next指针指向循环节点的值。
     *
     * @param args arg
     */
    public static void main(String[] args) {
        ListNode listNode1 = new ListNode(1);
        ListNode listNode2 = new ListNode(2);
        ListNode listNode3 = new ListNode(3);
        ListNode listNode4 = new ListNode(4);
        listNode1.next = listNode2;
        listNode2.next = listNode3;
        listNode3.next = listNode4;

        ListNode res = reverseList(listNode1);
        while (res != null) {
            System.out.print(res.val + " ");
            res = res.next;
        }
    }

    public static ListNode reverseList(ListNode head) {
        // 结果链表
        ListNode res = new ListNode();
        // 第一个节点特殊判断,作为新链表最后一个节点的值
        if (head != null) {
            res.val = head.val;
        } else {
            return null;
        }
        while (true) {
            if (head.next != null) {
                head = head.next;
                // 新节点作为父节点,t一直为循环至此的,结果的,第一个节点
                ListNode t = new ListNode();
                t.val = head.val;
                t.next = res;
                // res替换为新的第一个节点
                res = t;
            } else {
                break;
            }
        }
        return res;
    }

    static class ListNode {
        int val;
        ListNode next;

        ListNode() {
        }

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

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

912. 排序数组

题意:

给你一个整数数组 nums,请你将该数组升序排列。

思路:

快速排序。
快速排序是采用二分的思维方式,选中数组中的一个数字,以它为标准,左放比它小的数字,右边放大于它的数字,那么这个数字的位置就可以确定了,然后左边的数字、右边的数字重复这一操作,最终排序的数组为一个数字,那么排序也就完成了。
即每次只排一个数字。
优化:选取数字标准的时候,可以从数组中随机挑取,这样会加快排序速度。

代码解析:
import java.util.Arrays;
import java.util.Random;

public class QuickSort {

    private static final Random RANDOM = new Random();

    /**
     * 题意:给你一个整数数组 nums,请你将该数组升序排列。
     * 解析:快速排序。
     * 快速排序是采用二分的思维方式,选中数组中的一个数字,以它为标准,左放比它小的数字,右边放大于它的数字,那么这个数字的位置就可以确定了,然后左边的数字、右边的数字重复这一操作,最终排序的数组为一个数字,那么排序也就完成了。
     * 即每次只排一个数字。
     * 优化:选取数字标准的时候,可以从数组中随机挑取,这样会加快排序速度。
     *
     * @param args arg
     */
    public static void main(String[] args) {
        int[] nums = new int[]{5, 1, 1, 2, 0, 0};
        int[] ints = sortArray(nums);
        System.out.println(Arrays.toString(ints));
    }

    public static int[] sortArray(int[] nums) {
        // 快速排序
        return quickSort(nums);
    }

    /**
     * 快速排序
     *
     * @param nums 数组
     * @return 结果
     */
    private static int[] quickSort(int[] nums) {
        // 递归处理
        quickSortFun(nums, 0, nums.length - 1);
        return nums;
    }

    private static void quickSortFun(int[] nums, int left, int right) {
        // left,right分表代表着需要排序数组的区间,若不满足下属要求,说明已经不需要排序了
        if (left >= right) {
            return;
        }
        // 从数组中随机挑取一个数组与最左边的数字交换,即最左边为此次排序的标准
        int randomIndex = left + RANDOM.nextInt(right - left + 1);
        swap(nums, randomIndex, left);

        // 为何+1,是因为left数字为此次排序标准数,不参与排序,只需要最后放到它应该在的地方即可
        int i = left + 1;
        int j = right;
        int index = nums[left];
        while (true) {
            // 从左到右,比标准数小的话直接跳过,相等是不跳过的(原因如下),i是可以到达right的,
            while (i <= right && nums[i] < index) {
                i++;
            }
            // 从右到左,比标注数大的话直接跳过,相等是不跳过的(原因如下),j不允许到达left
            while (j > left && nums[j] > index) {
                j--;
            }
            // 限制左右循环不允许相遇(相遇说明排序完成了)
            if (i >= j) {
                break;
            }
            // i,j分别停留在了大于、小于标准数的位置,交换两个数字使数组满足要求
            // 相等的元素通过交换,等概率分到数组的两边
            swap(nums, i, j);
            i++;
            j--;
        }
        // 最终停留位置与标准数交换,那么标准数已经放到最终位置
        swap(nums, left, j);
        quickSortFun(nums, left, j - 1);
        quickSortFun(nums, j + 1, right);
    }

    private static void swap(int[] nums, int index1, int index2) {
        int temp = nums[index1];
        nums[index1] = nums[index2];
        nums[index2] = temp;
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值