LeetCode 1045, 14, 25

1045. 买下所有产品的客户

题目链接

1045. 买下所有产品的客户

  • Customer的字段为customer_idproduct_key
  • Product的字段为product_key

要求

  • 编写解决方案,报告 Customer 表中购买了 Product 表中所有产品的客户的 id。
  • 返回结果表 无顺序要求

知识点

  1. count():统计个数的函数。
  2. distinct:对某个字段进行去重。
  3. group by:根据某些字段进行分组。
  4. having:对分组后的结果进行筛选。

思路

购买了 Product 表中所有产品就是:顾客在 Customer 表中的购买记录产品种类数 = Product 表中的记录数。由于 Customer 表可能包含重复的行,所以不能统计顾客的购买记录数(即count(*)),而是要统计顾客的购买产品种类数(即count(distinct product_key)。Product 表中的记录数可以直接使用count(*)来获取,因为 product_keyProduct 表的主键(具有唯一值的列)。

代码

select
    customer_id
from
    Customer
group by
    customer_id
having
    count(distinct product_key) = (
        select
            count(*)
        from
            Product
    )

14. 最长公共前缀

题目链接

14. 最长公共前缀

标签

字典树 字符串

普通版

思路

可以将第一个字符串作为 模版字符串,与字符串数组剩下的字符串进行比较。本解法是这样看待字符串数组的:将每个字符串中的所有字符从左到右排列,将所有字符串从上到下排列,即对于字符串数组strs = ["flower","flow","flight"],有以下的排列:

f l o w e r
f l o w
f l i g h t

可以想到,解题的策略是:从左到右比较每一列的字符是否相同,如果不相同,则返回模版字符串的 索引为0 到 索引为上一列 的子字符串。但是要注意一点,如果比较的列数超过了字符串的长度时,应立即返回

对于上面这个字符串数组,先比较第一列,发现都是f;然后比较第二列,发现都是l;再比较第三列,发现第三个字符串的字符i与模版字符串的字符o不同,这时返回的子字符串的右端点索引是1。

在比较的时候,我选择将模版字符串转换成char[],因为这样做比使用StringcharAt()方法快。但是我没有将所有字符串数组都转换成char[],因为这样的浪费的空间太多了。

返回子字符串时,可以使用String的一个构造方法public String(char[] s, int i, int j),其中第一个参数是字符数组,第二个参数是从这个字符数组的哪个索引开始截取,第三个参数是以这个字符数组的哪个索引作为子字符串的结束索引,这个构造方法返回的字符串的区间是[i, j)(含左不含右)。例如new String(new char[]{'h', 'e', 'l', 'l', 'o'}, 1, 3)返回的字符串是"el"

代码

class Solution {
    public String longestCommonPrefix(String[] strs) {
        char[] s1 = strs[0].toCharArray(); // 将第一个字符串转换成字符数组
        for (int i = 0; i < s1.length; i++) { // i是列数
            for (int j = 1; j < strs.length; j++) { // j是字符串的索引,从1开始,避免让第一个字符串自己和自己比较
                // 当i >= strs[j].length()时,意味着这个字符串的长度不够了,该返回了
                // 当s1[i] != strs[j].charAt(i)时,意味着公共前缀不包含索引为i的字符,直接返回
                if (i >= strs[j].length() || s1[i] != strs[j].charAt(i)) {
                    return new String(s1, 0, i);
                }
            }
        }
        // 遍历完第一个字符串后,直接返回第一个字符串即可,第一个字符串就是公共前缀
        return strs[0];
    }
}

加强版

思路

对于“多个”这种字眼,我们可以采用 分治 的思路:每次都获取 左半区间 和 右半区间 内所有字符串的公共前缀,然后再求这两个公共前缀的公共前缀。直到区间只剩下一个字符串。

代码

class Solution {
    public String longestCommonPrefix(String[] strs) {
        return merge(strs, 0, strs.length - 1);
    }
    // 使用分治的思想求区间内的公共前缀
    private String merge(String[] strs, int i, int j) {
        if (i == j) {
            return strs[i];
        }
        int mid = i + j >> 1;
        String pre1 = merge(strs, i, mid); // 获取左半区间内所有字符串的公共前缀
        String pre2 = merge(strs, mid + 1, j); // 获取右半区间内所有字符串的公共前缀
        return commonPrefixTwo(pre1, pre2); // 求这两个公共前缀的公共前缀
    }
    // 求两个字符串的公共前缀
    private String commonPrefixTwo(String s1, String s2) {
        int minLen = Math.min(s1.length(), s2.length());
        int i = 0;
        while (i < minLen) {
            if (s1.charAt(i) != s2.charAt(i)) {
                break;
            }
            i++;
        }
        return s1.substring(0, i);
    }
}

25. K 个一组翻转链表

题目链接

25. K 个一组翻转链表

标签

递归 链表

思路

首先,这道题的难度为困难不是假的。其次,只要做过206. 反转链表, 24. 两两交换链表中的节点这两道题,对本题还是有一点思路的。其中,第二道题我写过文章:24. 两两交换链表中的节点。第一题很简单,相信大家看完本题后就会做了。

链表可以被分为长度为k的多段链表(最后一段长度如果不是k,则不需要反转),本题的操作可以理解为:反转多个长度为k的链表

反转一个链表有一种思路:将新链表从尾部开始建,每次都给出一个新节点插到头部,给出新节点的顺序是从旧链表的头部开始,直到旧链表的尾部。代码如下:

public ListNode reverseList(ListNode head) {
    ListNode new1 = null; // 新链表的头节点
    ListNode old1 = head; // 旧链表的头节点
    while (new1 != tail) { // 直到新链表的头节点等于旧链表的最后一个节点
        ListNode old2 = old1.next; // 先保存旧链表的第二个节点
        old1.next = new1; // 让旧链表的头节点指向新链表的头节点(反转的关键)
        new1 = old1; // 让新链表的头部向后移动一位
        old1 = old2; // 让旧链表的头部向后移动一位
    }
    return new1;
}

本题的反转链表有点不太一样,因为尾节点没有指向null,而是指向了一个节点,这时就需要传入两个值,一个是链表的头部head,一个是链表的尾部tail,进入循环的条件是 新链表的头节点 不等于 旧链表的尾节点,即new1 != tail

在反转完每段子链表时,子链表都从原链表中断开了,所以需要 重建子链表与原链表的关系,但有个前提——要知道四个节点:旧链表头节点的上一个节点prev、新链表的头节点head、新链表的尾节点tail、旧链表尾节点的下一个节点next。连接方式为:上一个节点指向新头节点prev.next = head,新尾节点指向下一个节点tail.next = next

要想反转这段链表,首先得保证这段链表的长度为k,并且还要找到尾节点,所以每次反转子链表前得先让尾节点的指针tail从旧链表的头部head开始,移动k步,如果遇到空节点,即tail == null,则返回结果即可,因为这段子链表的长度不足k

本题仍旧使用哨兵节点指向结果链表的头部的做法,因为前指针的初始值就是哨兵节点。

代码

class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode sentinel = new ListNode(-1, head); // 哨兵节点指向结果链表的头部,返回其next
        ListNode prev = sentinel; // 前指针,指向待反转链表的头节点

        while (prev != null) {
            // 先保证有 k 个节点,并且还得有尾节点,然后才能反转
            ListNode tail = prev;
            for (int i = 0; i < k; i++) {
                tail = tail.next;
                if (tail == null) { // 如果节点数不够,则直接返回结果
                    return sentinel.next;
                }
            }

            ListNode next = tail.next; // 反转前先保存原来尾节点的下一个节点
            
            // 进行反转
            ListNode[] reverse = reverse(head, tail); // 反转从头节点到尾节点的部分
            head = reverse[0]; // 获取新链表的头节点
            tail = reverse[1]; // 获取新链表的尾节点

            // 上面的操作会将部分链表从原始链表中截断,所以要重建节点之间的联系
            prev.next = head; // 让前一个节点指向新链表的头节点
            tail.next = next; // 让新链表的尾节点指向原来尾节点的下一个节点

            // 准备下一次的反转操作,更新指针的位置
            prev = tail; // 前指针移动到新链表的尾节点,即 指向待反转链表的头节点 的节点
            head = tail.next; // 头节点移动到新链表尾节点的下一个节点,即待反转链表的头节点
        }
        return sentinel.next;
    }
    // 新链表是从尾部开始建立,然后逐渐到头部
    private ListNode[] reverse(ListNode head, ListNode tail) {
        ListNode new1 = null; // 新链表的头节点
        ListNode old1 = head; // 旧链表的头节点
        while (new1 != tail) { // 直到新链表的头节点等于旧链表的最后一个节点
            ListNode old2 = old1.next; // 先保存旧链表的第二个节点
            old1.next = new1; // 让旧链表的头节点指向新链表的头节点(反转的关键)
            new1 = old1; // 让新链表的头部向后移动一位
            old1 = old2; // 让旧链表的头部向后移动一位
        }
        return new ListNode[]{tail, head}; // 返回新链表的头部和尾部
    }
}
  • 30
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值