3.11
数组
水果成篮
题目:
你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组
fruits
表示,其中fruits[i]
是第i
棵树上的水果 种类 。你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:
你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。
给你一个整数数组
fruits
,返回你可以收集的水果的 最大 数目。
思路:
-
设置一个哈希数组,里面对应类别希望保持最多两个(对应两个篮子),在大于两个时,移动窗口并统计最大窗口长度
-
设置滑动窗口,窗口移动right时统计到哈希数组中;若哈希数组超出2个,那就更新left,直到哈希数组中成为2个
class Solution {
public int totalFruit(int[] fruits) {
// fruits数组的长度
int n = fruits.length;
// 使用哈希表来存储当前窗口内各种水果的数量
Map<Integer, Integer> cnt = new HashMap<Integer, Integer>();
// 窗口的左边界
int left = 0;
// 记录遍历过程中满足条件的最大窗口的大小
int ans = 0;
// 开始遍历fruits数组
for(int right = 0; right < n; right++){
// 将当前水果的数量加1,如果该水果不存在,则先添加进哈希表
cnt.put(fruits[right], cnt.getOrDefault(fruits[right], 0) + 1);
// 当窗口内的不同水果种类数大于2时,需要缩小窗口
while(cnt.size() > 2){
// 将左边界的水果数量减1
cnt.put(fruits[left], cnt.get(fruits[left]) - 1);
// 如果某种水果的数量减到0,则将其从哈希表中移除
if(cnt.get(fruits[left]) == 0){
cnt.remove(fruits[left]);
}
// 缩小窗口,即将左边界向右移动
left++;
}
// 更新满足条件的最大窗口大小
ans = Math.max(ans, right - left + 1);
}
// 返回最大窗口的大小,即最多可以选择的水果数量
return ans;
}
}
二叉树
满二叉树
满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
完全二叉树
完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。
二叉搜索树
二叉搜索树是有数值的了,二叉搜索树是一个有序树。
-
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
-
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
-
它的左、右子树也分别为二叉排序树
平衡二叉搜索树
AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn,注意我这里没有说unordered_map、unordered_set,unordered_map、unordered_set底层实现是哈希表。
HashMap 和 HashSet:
底层实现:哈希表(自 Java 8 以来,当桶变得过于拥挤时,链表会转换成红黑树,以改善性能)。
时间复杂度:理想情况下,增删查操作的时间复杂度是 O(1)。但在最坏的情况下(例如,当哈希函数导致所有元素都映射到同一个桶中时),这些操作的时间复杂度会退化到 O(n)。
TreeMap 和 TreeSet:
底层实现:红黑树,这是一种自平衡的二叉搜索树。
时间复杂度:增删查操作的时间复杂度是 O(log n),因为红黑树保证了树的高度大致保持在 log n。
LinkedHashMap 和 LinkedHashSet:
底层实现:哈希表 + 双向链表。这种结构不仅保持了哈希表的快速访问特性,还能够维护元素的插入顺序或者最近最少使用(LRU)顺序。
时间复杂度:增删查操作的平均时间复杂度仍然是 O(1),但是它们保持了元素的插入顺序,使得遍历的时间复杂度是 O(n)。
其他注意事项:
ConcurrentHashMap
是一个线程安全的哈希表实现,使用分段锁(在 Java 8 以后通过使用 CAS 操作和 synchronized 来进一步优化)来提高并发性,其增删查操作的时间复杂度也是期望为 O(1)。
Hashtable
是一个遗留类,线程安全的哈希表实现,但它通过对整个哈希表加锁来实现线程安全,这导致了效率较低。现在推荐使用ConcurrentHashMap
来代替。
二叉树的存储方式
二叉树可以链式存储,也可以顺序存储。
那么链式存储方式就用指针, 顺序存储的方式就是用数组。
顾名思义就是顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在各个地址的节点串联一起。
链式存储:
链式存储是大家很熟悉的一种方式,那么我们来看看如何顺序存储呢?
其实就是用数组来存储二叉树,顺序存储:
用数组来存储二叉树如何遍历的呢?
如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
但是用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。
所以大家要了解,用数组依然可以表示二叉树。
二叉树的遍历方式
-
深度优先遍历:先往深走,遇到叶子节点再往回走。
前中后序指的就是中间节点的位置
-
前序遍历(递归法,迭代法)
-
中序遍历(递归法,迭代法)
-
后序遍历(递归法,迭代法)
-
-
广度优先遍历:一层一层的去遍历。
-
层次遍历(迭代法)
-
这两种遍历是图论中最基本的两种遍历方式
栈其实就是递归的一种实现结构,也就说前中后序遍历的逻辑其实都是可以借助栈使用递归的方式来实现的。
而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。
二叉树定义
public class TreeNode {
// 节点存储的值
int val;
// 指向左子节点的引用
TreeNode left;
// 指向右子节点的引用
TreeNode right;
// 无参构造函数。创建一个空的 TreeNode 对象。
TreeNode() {}
// 构造函数,只接受节点值。创建一个新的 TreeNode 对象,其值为传入的 val。
// 此时,左右子节点默认为 null。
TreeNode(int val) { this.val = val; }
// 构造函数,接受一个节点值和两个 TreeNode 引用。
// 创建一个新的 TreeNode 对象,其值为传入的 val,左子节点为传入的 left,右子节点为传入的 right。
// 这允许在创建节点的同时指定它的子节点,便于构建复杂的树结构。
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
二叉树的递归遍历
递归三要素
-
确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
-
确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
-
确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
前序遍历(Preorder Traversal)
前序遍历的顺序是:根节点 -> 左子树 -> 右子树。
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<Integer>();
// 递归进行前序遍历
preorder(root, result);
return result;
}
public void preorder(TreeNode root, List<Integer> result) {
if (root == null) {
// 如果当前节点为空,则返回,不进行任何操作
return;
}
// 首先访问当前节点
result.add(root.val);
// 递归访问左子树
preorder(root.left, result);
// 递归访问右子树
preorder(root.right, result);
}
}
中序遍历(Inorder Traversal)
中序遍历的顺序是:左子树 -> 根节点 -> 右子树。
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
// 递归进行中序遍历
inorder(root, res);
return res;
}
void inorder(TreeNode root, List<Integer> list) {
if (root == null) {
// 如果当前节点为空,则返回,不进行任何操作
return;
}
// 递归访问左子树
inorder(root.left, list);
// 访问当前节点
list.add(root.val);
// 递归访问右子树
inorder(root.right, list);
}
}
后序遍历(Postorder Traversal)
后序遍历的顺序是:左子树 -> 右子树 -> 根节点。
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
// 递归进行后序遍历
postorder(root, res);
return res;
}
void postorder(TreeNode root, List<Integer> list) {
if (root == null) {
// 如果当前节点为空,则返回,不进行任何操作
return;
}
// 递归访问左子树
postorder(root.left, list);
// 递归访问右子树
postorder(root.right, list);
// 访问当前节点
list.add(root.val);
}
}
这三种遍历方法各有特点,适用于不同的场景。前序遍历常用于打印树的结构,中序遍历用于二叉搜索树时可以得到有序的值,后序遍历常用于先处理子节点的情况,如计算一个节点的所有子树的信息。通过递归调用,每次递归都将问题规模缩小,直到遇到空节点为止。递归的基本思想是将大问题分解成小问题,再将小问题分解成更小问题,直到可以直接解决的程度。
SQL基础注意
牛客网在线编程SQL篇SQL必知必会 (nowcoder.com)
不重复
select distinct prod_id from OrderItems;
排序
-
降序DESC
-
升序ASC
select cust_name from Customers ORDER BY cust_name DESC;
-- 先按cust_id正排序,再按order_date倒排序
SELECT cust_id,order_num FROM Orders
order by cust_id,order_date desc;
-- 先后到排序
SELECT quantity,item_price FROM OrderItems
ORDER BY quantity DESC,item_price DESC;
包含某字符
-- 包含 like; 不包含 not like
select prod_name, prod_desc
from Products
where prod_desc like '%toy%' AND prod_desc LIKE '%carrots %';
-- 或者 where prod_desc like '%carrots%toy%';
组合
select prod_id as id, quantity
from OrderItems
where quantity = 100
union
select prod_id as id, quantity
from OrderItems
where prod_id like 'BNBG%'
order by id asc;
子查询
select cust_id,(
select sum(quantity*item_price) from OrderItems a
where a.order_num = b.order_num
) as total_ordered
from Orders b
order by total_ordered desc;
聚合
select order_num, sum(item_price * quantity) as total_price
from OrderItems a
group by order_num
having total_price >= 1000
order by order_num asc;
select vend_id, min(prod_price) as cheapest_item
from Products group by vend_id
order by cheapest_item asc;
联结表
INNER JOIN:只返回两个表中匹配条件的行。
LEFT JOIN(或 LEFT OUTER JOIN):返回左表中的所有行,即使右表中没有匹配的行。如果右表中没有匹配的行,则结果中右表的部分将为 NULL。
RIGHT JOIN(或 RIGHT OUTER JOIN):返回右表中的所有行,即使左表中没有匹配的行。如果左表中没有匹配的行,则结果中左表的部分将为 NULL。
FULL JOIN(或 FULL OUTER JOIN):返回左表和右表中的所有行。如果某一边没有匹配的行,则该边的部分将为 NULL。
CROSS JOIN:返回两个表的笛卡尔积,即第一个表中的每一行与第二个表中的每一行组合。
select
c.cust_name,
os.order_num,
sum(os.quantity * os.item_price) OrderTotal
from
Orders o
join OrderItems os on os.order_num = o.order_num
join Customers c on c.cust_id = o.cust_id
group by
c.cust_name,
os.order_num
order by
c.cust_name,
os.order_num;
创建高级联结
select
a.prod_name,
count(b.order_num) as orders
from
Products a
left join OrderItems b on a.prod_id = b.prod_id
group by
a.prod_name
order by
a.prod_name asc;