1045. 买下所有产品的客户
题目链接
表
- 表
Customer
的字段为customer_id
和product_key
。 - 表
Product
的字段为product_key
。
要求
- 编写解决方案,报告
Customer
表中购买了Product
表中所有产品的客户的 id。 - 返回结果表 无顺序要求 。
知识点
count()
:统计个数的函数。distinct
:对某个字段进行去重。group by
:根据某些字段进行分组。having
:对分组后的结果进行筛选。
思路
购买了 Product
表中所有产品就是:顾客在 Customer
表中的购买记录产品种类数 = Product
表中的记录数。由于 Customer
表可能包含重复的行,所以不能统计顾客的购买记录数(即count(*)
),而是要统计顾客的购买产品种类数(即count(distinct product_key
)。Product
表中的记录数可以直接使用count(*)
来获取,因为 product_key
是 Product
表的主键(具有唯一值的列)。
代码
select
customer_id
from
Customer
group by
customer_id
having
count(distinct product_key) = (
select
count(*)
from
Product
)
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[]
,因为这样做比使用String
的charAt()
方法快。但是我没有将所有字符串数组都转换成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 个一组翻转链表
题目链接
标签
递归 链表
思路
首先,这道题的难度为困难不是假的。其次,只要做过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}; // 返回新链表的头部和尾部
}
}