开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

MySQL索引的理解(主键索引和二级索引)

发表于 2021-11-17

MySQL索引的理解(主键索引和二级索引)

备注:一和二体现了主次和先后关系,聚焦和非聚焦体现不出来,
我建议称为:“一级索引”和“二级索引”。

1、一级索引
索引和数据存储在一起,都存储在同一个B+tree中的叶子节点。一般主键索引都是一级索引。

2、二级索引
二级索引树的叶子节点存储的是主键而不是数据。也就是说,在找到索引后,得到对应的主键,再回到一级索引中找主键对应的数据记录。

1
2
3
复制代码 索引是一种用于快速查询行的数据结构,就像一本书的目录就是一个索引,如果想在一本书中找到某个主题,
 一般会先找到对应页码。在mysql中,存储引擎用类似的方法使用索引,先在索引中找到对应值
 然后根据匹配的索引记录找到对应的行。

  我们首先了解一下索引的几种类型和索引的结构。

索引类型

B树

1
2
3
css复制代码大多数存储引擎都支持B树索引。b树通常意味着所有的值都是按顺序存储的,并且每一个叶子也到根的距离相同。
B树索引能够加快访问数据的速度,因为存储引擎不再需要进行全表扫描来获取数据。
下图就是一颗简单的B树。

在这里插入图片描述
B树的查询流程:
如上图我要从找到E字母,查找流程如下:
(1)获取根节点的关键字进行比较,当前根节点关键字为M,E<M(26个字母顺序),所以往找到指向左边的子节点(二分法规则,左小右大,左边放小于当前节点值的子节点、右边放大于当前节点值的子节点);
(2)拿到关键字D和G,D<E<G 所以直接找到D和G中间的节点;
(3)拿到E和F,因为E=E 所以直接返回关键字和指针信息(如果树结构里面没有包含所要查找的节点则返回null);
(4)通过指针信息取出这条记录的所有信息;

B+树

下图为B+树的结构,B+树是B树的升级版,我们可以观察一下,B树和B+树的区别是什么?
在这里插入图片描述
B+树和B树的区别是:

  1. B树的节点中没有重复元素,B+树有。
  2. B树的中间节点会存储数据指针信息,而B+树只有叶子节点才存储。
  3. B+树的每个叶子节点有一个指针指向下一个节点,把所有的叶子节点串在了一起。

从下图我们可以直观的看到B树和B+树的区别:紫红色的箭头是指向被索引的数据的指针,大红色的箭头即指向下一个叶子节点的指针。
在这里插入图片描述
我们假设被索引的列是主键,现在查找主键为5的记录,模拟一下查找的过程:

B树,在倒数第二层的节点中找到5后,可以立刻拿到指针获取行数据,查找停止。

B+树,在倒数第二层的节点中找到5后,由于中间节点不存有指针信息,则继续往下查找,在叶子节点中找到5,拿到指针获取行数据,查找停止。

B+树每个父节点的元素都会出现在子节点中,是子节点的最大(或最小)元素。叶子节点存储了被索引列的所有的数据。

那B+树比起B树有什么优点呢?

  1. 由于中间节点不存指针,同样大小的磁盘页可以容纳更多的节点元素,树的高度就小。(数据量相同的情况下,B+树比B树更加“矮胖”),==查找起来就更快。==
  2. B+树每次查找都必须到叶子节点才能获取数据,而B树不一定,B树可以在非叶子节点上获取数据。因此B+树查找的时间更稳定。
  3. B+树的每一个叶子节点都有指向下一个叶子节点的指针,==方便范围查询和全表查询==:只需要从第一个叶子节点开始顺着指针一直扫描下去即可,而B树则要对树做中序遍历。

了解了B+树的结构之后,我们对一张具体的表做分析:

1
2
3
4
5
6
7
sql复制代码create table Student(
last_name varchar(50) not null,
first_name varchar(50) not null,
birthday date not null,
gender int(2) not null,
key(last_name, first_name, birthday)
);

对于表中的每一行数据,索引中包含了name,birthday列的值。下图显示了该索引的结构:
在这里插入图片描述
索引对多个值进行排序的依据是create table语句中定义索引时列的顺序,即如果名字相同,则根据生日来排序。

B+树的结构决定了这种索引对以下类型的查询有效:

全值匹配
和索引中所有的列进行匹配,例如查找姓名为Cuba Allen,生日为1960-01-01的人。

匹配最左前缀
查找姓为Allen的人,即只用索引的第一列。

匹配列前缀
匹配某一列的值的开头部分,例如查找所有以J开头的姓的人。

匹配范围值
查找姓在Allen和Barrymore之间的人。

精确匹配某一列并范围匹配另外一列
查找姓为Allen,名字是字母K开头的人。即第一列last_name全匹配,第二列first_name范围匹配。

只访问索引的查询
查询只需要访问索引,无需访问数据行。这种索引叫做覆盖索引。

一些限制:

  • 如果不是按照索引的最左列开始查找,无法使用索引。例如上面例子中的索引无法用于查找某个特定生日的人,因为生日不是最左数据列。也不能查找last_name以某个字母结尾的人。
  • 不能跳过索引的列。上述索引无法用于查找last_name为Smith并且某个特定生日的人。如果不指定first_name,则mysql只能使用索引的第一列。
  • 如果查询中有某个列的范围查询,则右边所有的列都无法使用索引优化查找。

例如查询WHERE last_name=’Smith’ AND first_name LIKE ‘J%’ AND birthday=‘1996-05-19’,这个查询只能使用索引的前两列。

哈希索引
  哈希索引,只有精确匹配索引所有列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。如果多个列的哈希值相同,索引会以链表的方式存放多个指针记录到同一个哈希条目中。
因为索引自身只存储对应的哈希值,所以索引的结构十分紧凑,哈希索引查找的速度非常快。但是哈希索引也有它的限制:

  • 哈希索引不是按照索引顺序存储的,无法用于排序。
  • 不支持部分索引列匹配查找。
  • 不支持范围查找。

聚集索引(clusterd index)
 每个存储引擎为InnoDB的表都有一个特殊的索引,叫聚集索引。聚集索引并不是一种单独的索引类型,而是一种数据存储方式。当表有聚集索引的时候,它的数据行实际上存放在叶子页中。一个表不可能有两个地方存放数据,所以一个表只能有一个聚集索引。
  因为是存储引擎负责实现索引,因此不是所有的存储引擎都支持聚集索引。InnoDB表中聚集索引的索引列就是主键,所以聚集索引也叫主键索引。
例如下面这张InnoDB表:

1
2
3
4
5
6
sql复制代码create table Student(
id int(11) primary key auto_increment,
last_name varchar(50) not null,
first_name varchar(50) not null,
birthday date not null
);

聚集索引(主键索引)的结构如下图:
在这里插入图片描述
这是一棵B+树,它的叶子页包含了行的全部数据,节点页只包含了索引列(即主键)。

二级索引(secondary indexes)

  对于InnoDB表,在非主键列的其他列上建的索引就是二级索引(因为聚集索引只有一个)。二级索引可以有0个,1个或者多个。二级索引和聚集索引的区别是什么呢?二级索引的节点页和聚集索引一样,只存被索引列的值,而二级索引的叶子页除了索引列值,还存这一列对应的主键值。   

InnoDB和MyISAM的数据分布对比

以下表为例,我们看下InnoDB和MyISAM是如何存储这个表的:

1
2
3
4
5
scss复制代码create table layout_test(
col1 int(11) primary key,
col2 int(11) not null,
key(col2)
);

在这里插入图片描述
InnoDB表的数据分布
聚集索引(主键索引)分布如下:
在这里插入图片描述
可以看到,叶子节点存储了整个表的数据,而不是只有索引列,每个叶子节点包含了主键值、事务ID、用于事务和MVCC的回滚指针以及所有的剩余列(col2)。

二级索引分布如下:
在这里插入图片描述
二级索引的叶子节点中存储的不是“行指针”,而是主键值,并以此作为指向行的“指针”。这样的策略减少了当出现行移动或者数据页分裂时二级索引的维护工作。使用主键当做指针会让二级索引占更多空间,但好处是InnoDB在移动行时无需更新二级索引中的这个指针。

MyISAM表的数据分布

col1列上的索引:
在这里插入图片描述
col2列上的索引:
在这里插入图片描述
实际上MyISAM中主键索引和其他索引在结构上没有什么不同。

从下图可以看出InnoDB和MyISAM保存数据和索引的区别。
在这里插入图片描述
聚集索引的优点:

  • 可以把相关数据保存在一起,例如实现电子邮箱时,根据用户ID来聚集数据,读取少数的数据页就能获取某个用户的全部邮件。
  • 聚集索引将索引和数据保存在同一个B树中,因此从聚集索引中获取数据比在非聚集索引中要快一些。

聚集索引的缺点:

  • 插入速度严重依赖插入顺序。按照主键的顺序插入是加载数据到InnoDB表中速度最快的方式。假如磁盘中的某一个已经存满了,但是新增的行要插入到这一页当中,存储引擎就会把该也分裂成两个页面来容纳该行,这就是一次页分裂操作。页分裂会导致表占用更多的磁盘空间。
  • 更新聚集索引列的代价很高,会强制InnoDB将每个被更新的行移动到新的位置。
  • 用二级索引访问数据需要两个索引查找,不是一次。因为要先从二级索引的叶子节点获得主键值,再根据这主键去聚集索引中查到对应的行,所以需要两次B树查找。

顺序主键的策略:
  在InnoDB表中使用自增主键是既简单性能又高的策略,这样可以保证数据按顺序写入。最好避免随机的聚集索引,从性能的角度考虑,使用UUID来作为聚集索引是很糟糕的,这样不仅插入行花费的时间长,而且索引占用的空间也更大。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

30 如何使用Redis实现分布式锁? 基本概念 单机上

发表于 2021-11-17

基本概念

  • 什么是分布式锁
+ 如果需要控制分布式系统,不能是某个客户端本地的锁
+ 锁是保存在一个共享存储系统中的,可以被多个客户端共享访问和获取\
  • 为什么选择redis
+ 可以被多个客户端共享访问,正好就是一个共享存储系统\
+ 读写性能高,可以应对高并发的锁操作场景

单机上的锁和分布式锁的联系与区别

  • 对于单机锁
+ 变量为0,表示线程未获取到锁
+ 变量为1,表示已经有线程获取到锁
+ 获取锁则把变量置为1
+ 释放锁则把变量置为0
  • 分布式锁
+ 也是通过判断锁的变量值,多个客户端存储数据
+ 新的要求


    - 分布式锁的加锁和释放锁的过程,涉及多个操作(需要保证原子性)\
    - 怎么保证分布式锁的稳定性和可靠性

基于单个 Redis 节点实现高可靠的分布式锁

  • 设置设计分布式锁
+ key锁的名称 value锁的状态
  • 怎么保证读取锁,判断锁和设置锁操作的原子性
+ 使用redis原子命令


    - SETNX 如果存在就设置,如果不存在就不设置+DEL删除释放锁\
+ 使用Lua脚本
  • 使用SETNX的潜在风险
+ DEL操作异常,导致锁一直不释放  通过设置过期时间避免死锁
+ 不是申请的锁的客户端执行DEL会误删除锁 可以将value设为客户端唯一值
+ SET 命令的 NX 和 EX/PX 选项\
  • DEL删除释放锁\
+ 最好使用lua脚本,保证指令的原子性
+ 读取数据,删除释放数据

\

基于多个 Redis 节点实现高可靠的分布式锁

  • Redis 的开发者 Antirez 提出了分布式锁算法 Redlock\
  • 如果客户端能够和半数以上的实例成功地完成加锁操作\
+ 获取锁成功
+ 否则获取锁失败
  • 具体实现步骤
+ 客户端获取当前时间
+ 客户端按顺序依次向 N 个 Redis 实例执行加锁操作(使用指令SETNX value是客户端标识) 顺序加锁\
+ 完成加锁,计算总耗时 判断是否成功


    - 超过半数加锁成功
    - 总耗时未超过过期时间
  • 可以通过redlock保证可靠性

总结

  • 加锁注意点
+ 加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作 使用SETNX指令\
+ 锁变量需要设置过期时间,避免死锁\
+ 锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作\
  • 释放锁注意点
+ 释放锁也包含了读取锁变量值、判断锁变量值和删除锁变量三个操作\
+ 没有指令能够完成
+ 一般使用lua脚本实现操作的原子性
  • 保证稳定性
+ 可以使用Redlock算法,实现基于多个实例的分布式锁

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JDK18体验版已出,你还在用哪个版本?

发表于 2021-11-17

关注微信公众号可以查看面试经验: 为了offer

d135864a77fc06eea28fb6269334e53.jpg

qrcode_for_gh_5dab503ac197_258.jpg

​2021年11月11日jdk18体验版本已经可以下载,下载地址:jdk.java.net/18/

此次体验版的更新如下:

  • 默认为 UTF-8

jdk18之前的字符集取决于你的环境,jdk18指定 UTF-8 作为标准 Java API 的默认字符集。通过此更改,依赖于默认字符集的 API 将在所有实现、操作系统、区域设置和配置中保持一致。

  • 简单的网络服务器

提供一个命令行工具来启动一个只提供静态文件的最小网络服务器。没有 CGI 或类似 servlet 的功能可用。该工具对于原型设计、临时编码和测试目的非常有用。

  1. 描述
Simple Web Server 是一个命令行工具,用于为单个目录层次结构提供服务。它基于com.sun.net.httpserver自 2006 年以来包含在 JDK中的包中的 Web 服务器实现。该包得到官方支持,我们使用 API 对其进行了扩展,以简化服务器创建并增强请求处理。
  1. 命令行工具
以下命令通过运行jdk.httpserver模块的主类来启动 Simple Web Server :





$ java -m jdk.httpserver


如果启动成功,那么服务器会向 System.out 打印一条消息,列出本地地址和所服务目录的绝对路径。例如:





$ java -m jdk.httpserver


Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".


Serving /cwd and subdirectories on 127.0.0.1 port 8000


URL: <http://127.0.0.1:8000/>


默认情况下,服务器在前台运行并绑定到环回地址和端口 8000。这可以通过-b和-p选项进行更改。例如,要在端口 9000 上运行,请使用:





$ java -m jdk.httpserver -p 9000


例如,将 Simple Web Server 绑定到所有接口:





$ java -m jdk.httpserver -b 0.0.0.0


Serving /cwd and subdirectories on 0.0.0.0 (all interfaces) port 8000


URL: <http://123.456.7.891:8000/>


默认情况下,文件从当前目录提供。可以使用该-d选项指定不同的目录。





仅提供幂等 HEAD 和 GET 请求。任何其他请求都会收到501 - Not Implemented或405 - Not Allowed响应。GET 请求被映射到所服务的目录,如下所示:





如果请求的资源是文件,则提供其内容。


如果请求的资源是包含索引文件的目录,则提供索引文件的内容。


否则,列出目录的所有文件和子目录的名称。未列出或提供符号链接和隐藏文件。


服务器仅支持 HTTP。不支持 HTTPS。
  • Java API 文档中的代码片段

引入了一个新的内联标记 ,{@snippet …}来声明要出现在生成的文档中的代码片段。它可用于声明内联片段(其中代码片段包含在标签本身中)和 外部片段(其中代码片段从单独的源文件中读取

  1. 内联
1
2
3
4
5
6
7
8
markdown复制代码/**
* The following code shows how to use {@code Optional.isPresent}:
* {@snippet :
* if (v.isPresent()) {
* System.out.println("v: " + v.get());
* }
* }
*/
2. 外联
1
2
3
4
perl复制代码/**
* The following code shows how to use {@code Optional.isPresent}:
* {@snippet file="ShowOptional.java" region="example"}
*/

ShowOptional.java包含以下内容的文件在哪里:

1
2
3
4
5
6
7
8
9
csharp复制代码public class ShowOptional {
void show(Optional<String> v) {
// @start region="example"
if (v.isPresent()) {
System.out.println("v: " + v.get());
}
// @end
}
}

**

**

  • 使用方法句柄重新实现核心反射

这个就是重新实现了底层的反射,API还是没有变

  • Vector API(第三孵化器)

引入 API 来表达向量计算,这些计算在运行时可靠地编译为支持的 CPU 架构上的最佳向量指令,从而实现优于等效标量计算的性能

  • 互联网地址解析 SPI

定义用于主机名和地址解析的服务提供者接口 (SPI),以便java.net.InetAddress可以使用平台内置解析器以外的解析器

描述

该InetAddressAPI定义了查找操作多种方法:

InetAddress::getAllByName执行正向查找,将主机名映射到一组 IP 地址。

InetAddress::getByName 还执行前向查找,将主机名映射到其地址集中的第一个地址。

InetAddress::getCanonicalHostName执行反向查找,将 IP 地址映射到完全限定的域名。例如:

var addressBytes = new byte[] { (byte) 192, 0, 43, 7};

var resolvedHostName = InetAddress.getByAddress(addressBytes)

.getCanonicalHostName();

InetAddress::getHostName 如果需要,还会执行反向查找。

默认情况下,InetAddress使用操作系统的本机解析器来执行查找。可以缓存该查找的结果,无论是正的还是负的,以避免对同一主机的进一步查找。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【算法系列】Array算法题详细解析

发表于 2021-11-17

这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战

数组相关的算法题目是比较高频的,通过本文希望能够对你数组相关的算法能力有所帮助。

找出给定数组中的最大和最小元素

问题描述:

给你一个数字数组。你需要在数组中找到最小和最大的数。

思路:

  • 定义两个变量max,min,用来存放最大和最小元素
  • 遍历数组
    • 如果当前元素大于max,则将当前元素赋值给max
    • 如果当前元素小于min,则将当前元素赋值给min
  • 最后max和min则是最大和最小的元素

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class FindLargestSmallestNumberMain {

public static void main(String[] args) {

int arr[] = new int[]{12,56,76,89,100,343,21,234};
//将第一个元素赋值给
int min = arr[0];
int max = arr[0];

for(int i=1; i< arr.length; i++)
{
if(arr[i] > max)
max = arr[i];
else if (arr[i] < min)
min = arr[i];
}
System.out.println("Smallest Number is : " + min);
System.out.println("max Number is : " + max);
}
}

查找数组中缺少的数字

问题描述:

给定一个包含1到n的整数递增数组,但数组中从1到n的一个数字缺失了。你需要提供一个最佳的解决方案来找到丢失的数字。数字不会在数组中重复。

例如:

1
2
3
4
java复制代码int[] arr1={7,5,6,1,4,2};
Missing number : 3
int[] arr2={5,3,1,2};
Missing number : 4

思路:

  • 因为数组中元素是递增的,所以可以使用公式n=n*(n+1)/2求n个数的和
  • 计算出给定数组中元素的和
  • 使用n个数的和减去数组中元素的和结果便是缺少的数字

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public class MissingNumberMain {

public static void main(String[] args) {

int[] arr1={7,5,6,1,4,2};
System.out.println("Missing number from array arr1: "+missingNumber(arr1));
int[] arr2={5,3,1,2};
System.out.println("Missing number from array arr2: "+missingNumber(arr2));

}

public static int missingNumber(int[] arr)
{
int n=arr.length+1;
int sum=n*(n+1)/2;
int restSum=0;
for (int i = 0; i < arr.length; i++) {
restSum+=arr[i];
}
int missingNumber=sum-restSum;
return missingNumber;
}
}

从旋转的有序数组中查找元素

问题描述:

给定一个排序和旋转的数组,如下所示:

1
java复制代码int arr[]={16,19,21,25,3,5,8,10};

该数组是从一个有序数组arr[]={3,5,8,10,16,19,21,25}旋转后所得。

要求在在O(log n)时间复杂度的数组中搜索到一个指定元素。

思路:

  • 如果直接遍历数组,则时间复杂度为O(n),不符合题目要求;
  • 因为数组是由有序数组旋转所得,所以在某个下标的之前和之后是有序的;
  • 然后按照二分法查找。

算法逻辑:

  • 计算出中位下标mid=(low+high)/2;
  • 如果arr[mid]等于要查找的数字则返回;
  • 如果[low…mid]是有序的;
    • 如果要查找的数字在[low…mid],则high=mid-1;
    • 如果要查找的数字不在[low…mid],则low=mid+1;
  • 如果[mid…high]是有序的;
    • 如果要查找的数字在[mid…high],则low=mid+1;
    • 如果要查找的数字不在[mid…high],则high=mid-1;

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码public class SearchElementSortedAndRotatedArrayMain {

public static void main(String[] args) {
int arr[] = {16, 19, 21, 25, 3, 5, 8, 10};
System.out.println("Index of element 5 : " + findElementRotatedSortedArray(arr, 0, arr.length - 1, 5));
}

public static int findElementRotatedSortedArray(int[] arr, int low, int high, int number) {
int mid;
while (low <= high) {
mid = low + ((high - low) / 2);

if (arr[mid] == number) {
return mid;
}
if (arr[mid] <= arr[high]) {
// 右边部分是有序的
if (number > arr[mid] && number <= arr[high]) {
low = mid + 1;
} else {
high = mid - 1;
}
} else {
// 左边部分是有序的
if (arr[low] <= number && number < arr[mid]) {
high = mid - 1;
} else {
low = mid + 1;
}
}
}
return -1;
}
}

找到有序旋转数组中的最小元素

问题描述:

有序旋转数组定义同上一题,例如:

1
java复制代码int arr[]={16,19,21,25,3,5,8,10};

同样要求时间复杂度为O(log n)。

思路:

与上题类似,因数组可以拆分为前后两个有序数组,可以通过二分查找法的变体来完成;

  • 计算出中位下标mid=(low+high)/2;
  • 如果[mid…high]是有序的;
    • 最小值在右边,high=mid;
  • 否则最小值在左边,low= mid+1;

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class MinimumElementSortedAndRotatedArrayMain {

public static void main(String[] args) {
int arr[] = {16, 19, 21, 25, 3, 5, 8, 10};
System.out.println("Minimum element in the array : " + findMinimumElementRotatedSortedArray(arr, 0, arr.length - 1));
}
public static int findMinimumElementRotatedSortedArray(int[] arr, int low, int high) {
int mid;
while (low < high) {
mid = low + ((high - low) / 2);
if (arr[mid] < arr[high]) {
high = mid;
} else {
low = mid+1;
}
}
return arr[low];
}
}

查找数组中第二大的与元素

问题描述:

给定一个旋转有序数组,如下所示:

1
java复制代码int[] arr1={7,5,6,1,4,2};

找出第二大的元素:6

思路:

可以对数组排序,然后返回数组中的倒数第二个元素,时间复杂度为O(nlogn)。

  • 定义最大值highest 和第二大值变量secondHighest
  • 对数组进行遍历
  • 如果当前元素大于最大值
    • 将highest赋值给secondHighest
    • 将当前元素赋值给highest
  • 否则,如果当前元素大于secondHighest
    • 将当前元素赋值给secondHighest

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码public class FindSecondLargestMain {
public static void main(String args[]) {
int[] arr1 = {7, 5, 6, 1, 4, 2};
int secondHighest = findSecondLargestNumberInTheArray(arr1);
System.out.println("Second largest element in the array : " + secondHighest);
}

public static int findSecondLargestNumberInTheArray(int array[]) {
int highest = Integer.MIN_VALUE;
int secondHighest = Integer.MIN_VALUE;

// 遍历数组
for (int i = 0; i < array.length; i++) {
// 如果当前值大于最大值
if (array[i] > highest) {
// 最大值赋值给第二大值
secondHighest = highest;
// 当前值赋值给最大值
highest = array[i];
} else if (array[i] > secondHighest && array[i] != highest)
// 将当前值赋值给第二大值
secondHighest = array[i];
}
return secondHighest;
}
}

找出数组中出现奇数次的数

题目描述:

给定一个整数数组, 只有一个数字出现奇数次,其他数字都出现偶数次。 你需要找到奇数次出现的数字。 需要用O(n)时间复杂度和O(1)空间复杂度来解决它。

思路1:

暴力解法:使用两层循环,记录每个元素出现的次数,这种方法的时间复杂度为O(n^2),不是最优解。

思路2:

使用Hash,将数字作为key,出现的次数作为value,每当key重复时,value+1,这种解法的时间复杂度为O(n),但是空间复杂度也为O(n),不符合要求。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码int getOddTimesElementHashing(int ar[]) {
int i;
HashMap<Integer, Integer> elements = new HashMap<Integer, Integer>();
for (i = 0; i < ar.length; i++) {
int element = ar[i];
if (elements.get(element) == null) {
elements.put(element, 1);
} else{
elements.put(element, elements.get(element) + 1);
}
}
for (Entry<Integer, Integer> entry : elements.entrySet()) {
if (entry.getValue() % 2 == 1) {
return entry.getKey();
}
}
return -1;
}

思路3:

基于位运算异或操作。

异或运算:相同为0,不同为1。如a=1001,b=1010,则a^b=0010,a^a=0000。

按照题目描述,数组中只有1个数字出现奇数次,其他数字都是偶数次,则将所有数字异或运算后的结果就是出现奇数次的数字。

代码实现:

1
2
3
4
5
6
7
8
java复制代码int getOddTimesElement(int arr[]) {
int i;
int result = 0;
for (i = 0; i < arr.length; i++) {
result = result ^ arr[i];
}
return result;
}

该解法的时间复杂度为O(n);因为只使用了一个变量result,所以空间复杂度为O(1)。

计算火车站需要的最少站台数

题目描述:

给定两个数组,分别对应火车站车辆到达的时间和出站的时间,计算出火车站最少需要几个站台。

1
2
3
4
5
java复制代码// 到达时刻表
arrival[] = {1:00, 1:40, 1:50, 2:00, 2:15, 4:00}
// 出站时刻表
departure[] = {1:10, 3:00, 2:20, 2:30, 3:15, 6:00}
// 最少需要的站台数 = 4

思路1:

遍历两个数组,检查每个到达和出站的时间间隔有多少重叠。

这种方式的时间复杂度为O(n^2),不是最优解。

思路2:

使用归并排序的逻辑。

  • 对到达和出发的数组进行排序;
  • 因为到达和出站数量一样,则从两个数组中取出相同位置的元素比较时间大小;
  • 如果到达时间早,则站台数加1;
  • 如果出站时间早,则站台数减1;
  • 在这个过程中记录最大的站台数;
  • 最后返回最大站台数的值。

这种算法的时间复杂度为O(n*log n)。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码public class TrainPlatformMain {
public static void main(String args[]) {
// arr[] = {1:00, 1:40, 1:50, 2:00, 2:15, 4:00}
// dep[] = {1:10, 3:00, 2:20, 2:30, 3:15, 6:00}
int arr[] = {100, 140, 150, 200, 215, 400};
int dep[] = {110, 300, 210, 230, 315, 600};
System.out.println("最少需要的站台数 =" + findPlatformsRequiredForStation(arr, dep, arr.length));
}

static int findPlatformsRequiredForStation(int arr[], int dep[], int n) {
int platform_needed = 0, maxPlatforms = 0;
Arrays.sort(arr);
Arrays.sort(dep);
int i = 0, j = 0;
// 类似归并排序中的归并操作
while (i < n && j < n) {
if (arr[i] < dep[j]) {
platform_needed++;
i++;
if (platform_needed > maxPlatforms)
maxPlatforms = platform_needed;
} else {
platform_needed--;
j++;
}
}
return maxPlatforms;
}
}

数组中最两数之和最接近0的两个数

题目描述:

一个数组中有一系列的正数和负数,找出数组中两数之和最接近0的两个数。

例如:

1
2
java复制代码array[]={1,3,-5,7,8,20,-40,6};
// 和最接近0的两个数是 : -5 and 6

思路1:

暴力解法:将所有数字两两求和,如果和的绝对值更小,则赋值记录两数的值。

这种方法的时间复杂度为O(n^2),不是最优解。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public static void findPairWithMinSumBruteForce(int arr[]) {
if (arr.length < 2)
return;
// 预设前两个数的和最接近于0
int minimumSum = arr[0] + arr[1];
int pair1stIndex = 0;
int pair2ndIndex = 1;
for (int i = 0; i < arr.length; i++) {
for (int j = i + 1; j < arr.length; j++) {
int tempSum = arr[i] + arr[j];
if (Math.abs(tempSum) < Math.abs(minimumSum)) {
pair1stIndex = i;
pair2ndIndex = j;
minimumSum = tempSum;
}
}
}
System.out.println(" 和最接近0的两个数是 : " + arr[pair1stIndex] + " " + arr[pair2ndIndex]);
}

思路2:

  • 对数组进行排序;
  • 定义两个索引对应数组的开始位置l=0,一个在结束位置r=n-1;
  • 按照l<r的条件循环
  • 计算arr[l]+arr[r]的和sum
  • 如果abs(sum)<abs(minSum),将l和r记录在最小的两个数组元素变量上;
  • 如果sum>0,则sum要更接近0需要将大值往小移动,r–;
  • 如果sum<0,则sum要更接近0需要将小值往大移动,l++。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码public static void findPairWithMinSum(int arr[]) {
Arrays.sort(arr);
int sum = 0;
int minimumSum = Integer.MAX_VALUE;
int n = arr.length;
// left and right index variables
int l = 0, r = n - 1;

int minLeft = l, minRight = n - 1;

while (l < r) {
sum = arr[l] + arr[r];
// 如果abs(sum)<abs(minSum),将l和r记录在最小的两个数组元素变量上;
if (Math.abs(sum) < Math.abs(minimumSum)) {
minimumSum = sum;
minLeft = l;
minRight = r;
}
if (sum < 0)
l++;
else
r--;
}
System.out.println(" 和最接近0的两个数是 : " + arr[minLeft] + " " + arr[minRight]);
}

该算法的时间复杂度为O(NLogN)。

数组中最两数之和最接近X的两个数

该问题为上一题的进阶版,找出两数之和最接近X的数。

思路1:

暴力解法:两层循环,求和与最接近的数进行比较,如果更小则记录对应数字,时间复杂度为O(n^2),不是最优解。

思路2:

与上一题的区别在于该问题要对求和结果比较的值是指定的X,不是0,不能直接使用abs()函数的结果进行比较;那该如何处理呢?

可以通过将abs()结果减去X,则对应该问题的解法。

  • 对数组进行排序;
  • 定义两个索引对应数组的开始位置l=0,一个在结束位置r=n-1;
  • 按照l<r的条件循环
  • 计算arr[l]+arr[r]-x的结果diff
  • 如果abs(diff)<minDiff,将l和r记录在最小的两个数组元素变量上;
  • 如果sum>x,则sum要更接近x需要将大值往小移动,r–;
  • 如果sum<x,则sum要更接近x需要将小值往大移动,l++。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public static void findPairWithClosestToX(int arr[], int X) {
Arrays.sort(arr);
int minimumDiff = Integer.MAX_VALUE;
int n = arr.length;

int l = 0, r = n - 1;

int minLeft = l, minRight = n - 1;
while (l < r) {
int currentDiff = Math.abs(arr[l] + arr[r] - X);
// 如果abs(diff)<minDiff,将l和r记录在最小的两个数组元素变量上
if (currentDiff < minimumDiff) {
minimumDiff = currentDiff;
minLeft = l;
minRight = r;
}
if (arr[l] + arr[r] < X)
l++;
else
r--;
}
System.out.println(" 和最接近X的两个数是 : " + arr[minLeft] + " " + arr[minRight]);
}

数组中两数之和等于X的所有组合

题目描述:

给定一个数组和一个指定数字,查找出数组中所有两数之和等于指定数字的组合。

思路1:

两层循环暴力解法,时间复杂度低,不是最优解。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public static void findPairsWithSumEqualsToXBruteForce(int arr[], int X) {
if (arr.length < 2)
return;
System.out.println("暴力破解两数之和等于X的组合: ");
for (int i = 0; i < arr.length; i++) {
for (int j = i + 1; j < arr.length; j++) {
int tempSum = arr[i] + arr[j];

if (tempSum == X) {
System.out.println(arr[i] + " " + arr[j]);
}
}
}
}

思路2:

对数组进行排序;

从数组的两端进行遍历,直到相遇,使用l代表左边,r代表右边;

如果arr[l]+arr[r]=X,则符合条件,打印结果,l–,r–;

如果arr[l]+arr[r]>X,则表示结果偏大,需要将大值降低,r–;

如果arr[l]+arr[r]<X,则表示结果偏小,需要将小值升高,l++;

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public static void findPairsEqualsToX(int arr[], int X) {
int n = arr.length;
if (n < 2)
return;
Arrays.sort(arr);
System.out.println("两数之和等于X的组合: ");

int l = 0, r = n - 1;

while (l < r) {
int currentSum = arr[l] + arr[r];
if (currentSum == X) {
System.out.println(arr[l] + " " + arr[r]);
l++;
r--;
} else if (arr[l] + arr[r] < X)
l++;
else
r--;
}
}

该解法时间复杂度为:O(NLogN)。

思路3:

  • 使用HashMap,将数组元素的值作为key,下标作为value放入map中;
  • 遍历数组;
  • 如果X-arr[i]在HashMap中存在,则从Map中取出结果,代表符合条件。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public static void findPairsEqualsToXUsingHashing(int arr[], int X) {
HashMap<Integer, Integer> elementIndexMap = new HashMap<>();
System.out.println("两数之和等于X的组合: ");
for (int i = 0; i < arr.length; i++) {
elementIndexMap.put(arr[i], i);
}
for (int i = 0; i < arr.length; i++) {
if (elementIndexMap.get(X - arr[i]) != null && elementIndexMap.get(X - arr[i]) != i) //
{
System.out.println(arr[i] + " " + (X - arr[i]));
}
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

mybatis的collection的性能问题 前言 定位

发表于 2021-11-17

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

前言

本文记录一下,mybatis的collection使用不当,导致接口响应慢的问题。

过程是这样的,有一个分页接口,响应很慢。按道理一条sql的查询逻辑为啥会慢呢?

定位

所以我们开启了sql的日志打印,方便我们定位问题。将mapper的日志等级设置为debug模式即可。

1
2
3
4
yaml复制代码logging:
level:
root: info
mapper包名: debug

可以发现,第一行就是我们的主sql,一个列表条件查询。但是查询完列表sql后,后面又执行了多次子查询的sql。
image.png
现在就需要我们看一下mapper的xml是怎么写的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码<select id="getProductInfoList" resultMap="productMap">
select
t.*
from
type_product t
where 1=1 AND t.is_deleted = 'N'
<if test="productTypeId != null and productTypeId != '' ">
and t.product_type_id=#{productTypeId}
</if>
<if test="productTypeTagId != null and productTypeTagId != '' ">
and t.product_type_tag_id=#{productTypeTagId}
</if>
group by product_id
</select>

没啥问题,就是主条件查询的sql。那么后面这些子查询sql哪来的呢?我们再来看一下我们定义的resultMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<resultMap id="productMap" type="com.baoyun.iyb.app.dao.dataobject.ProductInfo">
<id column="id" property="id"></id>
<result column="product_name" property="productName"></result>
<result column="product_id" property="productId"></result>
<result column="description" property="description"></result>
<result column="product_img_url" property="productImgUrl"></result>
<result column="gmt_modified" property="gmtModified"></result>
<!-- 查询标签 -->
<collection property="tags" column="product_id" ofType="com.baoyun.iyb.app.dao.dataobject.Tag"
select="com.baoyun.iyb.app.dao.mapper.TagMapper.getTags">
<result column="tag_name" property="tagName"></result>
<result column="tag_title" property="tagTitle"></result>
<result column="tag_content" property="tagContent"></result>
</collection>
</resultMap>

发现用到了collection来表示一对多的关系。其中定义了select,这个TagMapper.getTags就是日志打印出来的那些子查询。

方案

找到原因后,就应该想一下如何去避免它。其实collection还有另外一种用法,就是在主sql上join关联表去查询,通过使用collection来对数据进行聚合,成一个list。给一个案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码<resultMap type="Student" id="StudentMap2">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="job" property="job" />
<collection property="scores" javaType="java.util.ArrayList" ofType="Score">
<id column="id" property="id" />
<result column="num" property="num" />
<association property="subject" javaType="Subject">
<id column="id" property="id" />
<result column="name" property="name" />
</association>
</collection>
</resultMap>
<select id="queryStudents2" resultMap="StudentMap2" >
SELECT stu.id,stu.name name,stu.job,sco.id id,sco.num num,sub.id id,sub.name name
FROM t_student stu LEFT JOIN t_score sco ON stu.id = sco.sid LEFT JOIN t_subject sub ON sco.subject = sub.id
</select>

这种方案可以看出只需要执行一次sql就行,缺点也很明显,就是代码的可重用性几乎没有了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

System Performance 读书笔记 - 操作系统

发表于 2021-11-17

这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战

本系列是针对 Systems Performance: Enterprise and the Cloud, 2nd Edition (2020) 书籍的读书笔记,加入了一些个人理解以及拓展,并且针对一些难以理解的地方提供了一些额外的参考

内核(Kernel)

经典模型中,内核在操作系统结构中的位置如图所示:

image

从里到外分别是:

  • 硬件(Hardware):操作系统运行在的硬件设备。
  • 内核(Kernel):操作系统的核心软件,内核管理着 CPU 调度、内存、文件系统、网络协议以及各种系统设备(磁盘 IO、网络 IO 等等)。通过系统调用提供服务。
  • 系统调用(System Calls):提供访问硬件设备或者内核服务的程序接口。例如 open, close, read, write, ioctl等,需包含头文件unistd.h。
  • 系统库(System Libraries):直接用系统调用可能不太方便,我们可以使用封装好的库函数进行编程使用。从图上可以看出,这里其实有个缺口,因为应用也可以不使用系统库而是直接使用系统调用。例如像是 Go 语言运行环境,他就使用了自己封装的系统调用层而不是标准库 libc。

目前很多操作系统都在这个模型的基础上做了变种,之后我们会详细分析。

内核执行

经过不断地迭代,内核目前已经非常庞大,有上百万的代码。内核的执行是按需的,例如当用户级别的应用程序发起了系统调用,或者设备发送了一个中断(interrupt)的时候。另外,某些内核线程回异步执行一些维护性的工作,可能包含内核时钟程序以及内存管理任务,但是这些任务都会尽量保持轻量级并只占用很少的 CPU 资源。

像 Web 服务器这种 I/O 密集型的应用(不断的接受请求返回响应),会经常在内核上下文中执行。计算密集型的应用则会尽量不打扰内核,可以不中断地在 CPU 上执行。内核调度器会决定那个线程会运行,哪个会等待,以及调度到哪个 CPU 上。内核会选择硬件缓存更热或者对于这个进程本地性更好的 CPU,来提高性能。

内核态以及用户态

内核态(kernel mode):运行内核程序的时候,CPU 处于的模式即内核态,在这一状态下,设备的一切访问以及各种特权命令执行都是被允许的。内核控制对于设备的访问来实现多进程处理。除非明确指定,否则进程之间或者用户之间的数据是无法互相访问的

用户态(user mode):运行用户程序的时候,CPU 处于的模式。通过系统调用,会从用户态切换到内核态用更高的权限级别执行:

image

用户态切换到内核态是一种模式切换(mode switch),所有的系统调用都会模式切换,某些系统调用还会上下文切换:遇到硬盘 IO 或者网络 IO 的线程会上下文切换到可以运行的线程。这种切换都是有性能损耗的,一般通过如下几种优化来避免:

  • 用户模式系统调用(User-mode syscalls):可以在用户模式库实现一些系统调用。Linux 通过暴露 virtual dynamic shared object (vDSO)来实现,可以参考:man7.org/linux/man-p…
  • 内存映射(Memory mappings):用于按需装载内存页(缺页中断),后面还会提到。这样能避免直接访问 IO 造成系统调用。
  • 内核绕开(Kernel bypass):可以让用户态程序直接访问设备,例如 DPDK(Data Plane Development Kit),这里推荐一篇关于 DPDK 的文章
  • 内核态应用:例如运行在内核的 TUX 服务器,以及 BPF(Berkeley Packet Filter). 关于 BPF,有一个著名的基于 BPF 实现的工具集合是:github.com/iovisor/bcc

link

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

手把手教你基于Netty实现一个基础的RPC框架(通俗易懂)

发表于 2021-11-17

「这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战」

阅读这篇文章之前,建议先阅读和这篇文章关联的内容。

[1]详细剖析分布式微服务架构下网络通信的底层实现原理(图解)

[2][年薪60W的技巧]工作了5年,你真的理解Netty以及为什么要用吗?(深度干货)

[3]深度解析Netty中的核心组件(图解+实例)

[4]BAT面试必问细节:关于Netty中的ByteBuf详解

[5]通过大量实战案例分解Netty中是如何解决拆包黏包问题的?

[6]基于Netty实现自定义消息通信协议(协议设计及解析应用实战)

[7]全网最详细最齐全的序列化技术及深度解析与应用实战

在前面的内容中,我们已经由浅入深的理解了Netty的基础知识和实现原理,相信大家已经对Netty有了一个较为全面的理解。那么接下来,我们通过一个手写RPC通信的实战案例来带大家了解Netty的实际应用。

为什么要选择RPC来作为实战呢?因为Netty本身就是解决通信问题,而在实际应用中,RPC协议框架是我们接触得最多的一种,所以这个实战能让大家了解到Netty实际应用之外,还能理解RPC的底层原理。

什么是RPC

RPC全称为(Remote Procedure Call),是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议,简单理解就是让开发者能够像调用本地服务一样调用远程服务。

既然是协议,那么它必然有协议的规范,如图6-1所示。

为了达到“让开发者能够像调用本地服务那样调用远程服务”的目的,RPC协议需像图6-1那样实现远程交互。

  • 客户端调用远程服务时,必须要通过本地动态代理模块来屏蔽网络通信的细节,所以动态代理模块需要负责将请求参数、方法等数据组装成数据包发送到目标服务器
  • 这个数据包在发送时,还需要遵循约定的消息协议以及序列化协议,最终转化为二进制数据流传输
  • 服务端收到数据包后,先按照约定的消息协议解码,得到请求信息。
  • 服务端再根据请求信息路由调用到目标服务,获得结果并返回给客户端。

1567677351249

图6-1
业内主流的RPC框架
==========

凡是满足RPC协议的框架,我们成为RPC框架,在实际开发中,我们可以使用开源且相对成熟的RPC框架解决微服务架构下的远程通信问题,常见的rpc框架:

  1. Thrift:thrift是一个软件框架,用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引擎,以构建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 这些编程语言间无缝结合的、高效的服务。
  2. Dubbo:Dubbo是一个分布式服务框架,以及SOA治理方案。其功能主要包括:高性能NIO通讯及多协议集成,服务动态寻址与路由,软负载均衡与容错,依赖分析与降级等。 Dubbo是阿里巴巴内部的SOA服务化治理方案的核心框架,Dubbo自2011年开源后,已被许多非阿里系公司使用。

手写RPC注意要点

基于上文中对于RPC协议的理解,如果我们自己去实现,需要考虑哪些技术呢? 其实基于图6-1的整个流程应该有一个大概的理解。

  • 通信协议,RPC框架对性能的要求非常高,所以通信协议应该是越简单越好,这样可以减少编解码带来的性能损耗,大部分主流的RPC框架会直接选择TCP、HTTP协议。
  • 序列化和反序列化,数据要进行网络传输,需要对数据进行序列化和反序列化,前面我们说过,所谓的序列化和反序列化是不把对象转化成二进制流以及将二进制流转化成对象的过程。在序列化框架选择上,我们一般会选择高效且通用的算法,比如FastJson、Protobuf、Hessian等。这些序列化技术都要比原生的序列化操作更加高效,压缩比也较高。
  • 动态代理, 客户端调用远程服务时,需要通过动态代理来屏蔽网络通信细节。而动态代理又是在运行过程中生成的,所以动态代理类的生成速度、字节码大小都会影响到RPC整体框架的性能和资源消耗。常见的动态代理技术: Javassist、Cglib、JDK的动态代理等。

基于Netty手写实现RPC

理解了RPC协议后,我们基于Netty来实现一个RPC通信框架。

代码详见附件 netty-rpc-example

image-20210907221358022

图6-2 项目模块组成
需要引入的jar包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>

模块依赖关系:

  • provider依赖 netty-rpc-protocol和netty-rpc-api
  • cosumer依赖 netty-rpc-protocol和netty-rpc-api

netty-rpc-api模块

image-20210907223045613

图6-3 netty-rpc-api模块组成

IUserService

1
2
3
4
java复制代码public interface IUserService {

String saveUser(String name);
}

netty-rpc-provider模块

image-20210907223111784

图6-4 netty-rpc-provider模块组成

UserServiceImpl

1
2
3
4
5
6
7
8
9
java复制代码@Service
@Slf4j
public class UserServiceImpl implements IUserService {
@Override
public String saveUser(String name) {
log.info("begin saveUser:"+name);
return "Save User Success!";
}
}

NettyRpcProviderMain

注意,在当前步骤中,描述了case的部分,暂时先不用加,后续再加上

1
2
3
4
5
6
7
8
9
java复制代码@ComponentScan(basePackages = {"com.example.spring","com.example.service"})  //case1(后续再加上)
@SpringBootApplication
public class NettyRpcProviderMain {

public static void main(String[] args) throws Exception {
SpringApplication.run(NettyRpcProviderMain.class, args);
new NettyServer("127.0.0.1",8080).startNettyServer(); //case2(后续再加上)
}
}

netty-rpc-protocol

开始写通信协议模块,这个模块主要做几个事情

  • 定义消息协议
  • 定义序列化反序列化方法
  • 建立netty通信

图6-5
定义消息协议


之前我们讲过自定义消息协议,我们在这里可以按照下面这个协议格式来定义好。

1
2
3
4
5
6
7
txt复制代码    /*
+----------------------------------------------+
| 魔数 2byte | 序列化算法 1byte | 请求类型 1byte |
+----------------------------------------------+
| 消息 ID 8byte | 数据长度 4byte |
+----------------------------------------------+
*/

Header

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@AllArgsConstructor
@Data
public class Header implements Serializable {
/*
+----------------------------------------------+
| 魔数 2byte | 序列化算法 1byte | 请求类型 1byte |
+----------------------------------------------+
| 消息 ID 8byte | 数据长度 4byte |
+----------------------------------------------+
*/
private short magic; //魔数-用来验证报文的身份(2个字节)
private byte serialType; //序列化类型(1个字节)
private byte reqType; //操作类型(1个字节)
private long requestId; //请求id(8个字节)
private int length; //数据长度(4个字节)

}

RpcRequest

1
2
3
4
5
6
7
java复制代码@Data
public class RpcRequest implements Serializable {
private String className;
private String methodName;
private Object[] params;
private Class<?>[] parameterTypes;
}

RpcResponse

1
2
3
4
5
6
java复制代码@Data
public class RpcResponse implements Serializable {

private Object data;
private String msg;
}

RpcProtocol

1
2
3
4
5
java复制代码@Data
public class RpcProtocol<T> implements Serializable {
private Header header;
private T content;
}

定义相关常量

上述消息协议定义中,涉及到几个枚举相关的类,定义如下

ReqType

消息类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public enum ReqType {

REQUEST((byte)1),
RESPONSE((byte)2),
HEARTBEAT((byte)3);

private byte code;

private ReqType(byte code) {
this.code=code;
}

public byte code(){
return this.code;
}
public static ReqType findByCode(int code) {
for (ReqType msgType : ReqType.values()) {
if (msgType.code() == code) {
return msgType;
}
}
return null;
}
}

SerialType

序列化类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public enum SerialType {

JSON_SERIAL((byte)0),
JAVA_SERIAL((byte)1);

private byte code;

SerialType(byte code) {
this.code=code;
}

public byte code(){
return this.code;
}
}

RpcConstant

1
2
3
4
5
6
java复制代码public class RpcConstant {
//header部分的总字节数
public final static int HEAD_TOTAL_LEN=16;
//魔数
public final static short MAGIC=0xca;
}

定义序列化相关实现

这里演示两种,一种是JSON方式,另一种是Java原生的方式

ISerializer

1
2
3
4
5
6
7
8
java复制代码public interface ISerializer {

<T> byte[] serialize(T obj);

<T> T deserialize(byte[] data,Class<T> clazz);

byte getType();
}

JavaSerializer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码public class JavaSerializer implements ISerializer{

@Override
public <T> byte[] serialize(T obj) {
ByteArrayOutputStream byteArrayOutputStream=
new ByteArrayOutputStream();
try {
ObjectOutputStream outputStream=
new ObjectOutputStream(byteArrayOutputStream);

outputStream.writeObject(obj);

return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return new byte[0];
}

@Override
public <T> T deserialize(byte[] data, Class<T> clazz) {
ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(data);
try {
ObjectInputStream objectInputStream=
new ObjectInputStream(byteArrayInputStream);

return (T) objectInputStream.readObject();

} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}

@Override
public byte getType() {
return SerialType.JAVA_SERIAL.code();
}
}

JsonSerializer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class JsonSerializer implements ISerializer{
@Override
public <T> byte[] serialize(T obj) {
return JSON.toJSONString(obj).getBytes();
}

@Override
public <T> T deserialize(byte[] data, Class<T> clazz) {
return JSON.parseObject(new String(data),clazz);
}

@Override
public byte getType() {
return SerialType.JSON_SERIAL.code();
}
}

SerializerManager

实现对序列化机制的管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class SerializerManager {

private final static ConcurrentHashMap<Byte, ISerializer> serializers=new ConcurrentHashMap<Byte, ISerializer>();

static {
ISerializer jsonSerializer=new JsonSerializer();
ISerializer javaSerializer=new JavaSerializer();
serializers.put(jsonSerializer.getType(),jsonSerializer);
serializers.put(javaSerializer.getType(),javaSerializer);
}

public static ISerializer getSerializer(byte key){
ISerializer serializer=serializers.get(key);
if(serializer==null){
return new JavaSerializer();
}
return serializer;
}
}

定义编码和解码实现

由于自定义了消息协议,所以 需要自己实现编码和解码,代码如下

RpcDecoder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
java复制代码@Slf4j
public class RpcDecoder extends ByteToMessageDecoder {


/*
+----------------------------------------------+
| 魔数 2byte | 序列化算法 1byte | 请求类型 1byte |
+----------------------------------------------+
| 消息 ID 8byte | 数据长度 4byte |
+----------------------------------------------+
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
log.info("==========begin RpcDecoder ==============");
if(in.readableBytes()< RpcConstant.HEAD_TOTAL_LEN){
//消息长度不够,不需要解析
return;
}
in.markReaderIndex();//标记一个读取数据的索引,后续用来重置。
short magic=in.readShort(); //读取magic
if(magic!=RpcConstant.MAGIC){
throw new IllegalArgumentException("Illegal request parameter 'magic',"+magic);
}
byte serialType=in.readByte(); //读取序列化算法类型
byte reqType=in.readByte(); //请求类型
long requestId=in.readLong(); //请求消息id
int dataLength=in.readInt(); //请求数据长度
//可读区域的字节数小于实际数据长度
if(in.readableBytes()<dataLength){
in.resetReaderIndex();
return;
}
//读取消息内容
byte[] content=new byte[dataLength];
in.readBytes(content);

//构建header头信息
Header header=new Header(magic,serialType,reqType,requestId,dataLength);
ISerializer serializer=SerializerManager.getSerializer(serialType);
ReqType rt=ReqType.findByCode(reqType);
switch(rt){
case REQUEST:
RpcRequest request=serializer.deserialize(content, RpcRequest.class);
RpcProtocol<RpcRequest> reqProtocol=new RpcProtocol<>();
reqProtocol.setHeader(header);
reqProtocol.setContent(request);
out.add(reqProtocol);
break;
case RESPONSE:
RpcResponse response=serializer.deserialize(content,RpcResponse.class);
RpcProtocol<RpcResponse> resProtocol=new RpcProtocol<>();
resProtocol.setHeader(header);
resProtocol.setContent(response);
out.add(resProtocol);
break;
case HEARTBEAT:
break;
default:
break;
}

}
}

RpcEncoder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码@Slf4j
public class RpcEncoder extends MessageToByteEncoder<RpcProtocol<Object>> {

/*
+----------------------------------------------+
| 魔数 2byte | 序列化算法 1byte | 请求类型 1byte |
+----------------------------------------------+
| 消息 ID 8byte | 数据长度 4byte |
+----------------------------------------------+
*/
@Override
protected void encode(ChannelHandlerContext ctx, RpcProtocol<Object> msg, ByteBuf out) throws Exception {
log.info("=============begin RpcEncoder============");
Header header=msg.getHeader();
out.writeShort(header.getMagic()); //写入魔数
out.writeByte(header.getSerialType()); //写入序列化类型
out.writeByte(header.getReqType());//写入请求类型
out.writeLong(header.getRequestId()); //写入请求id
ISerializer serializer= SerializerManager.getSerializer(header.getSerialType());
byte[] data=serializer.serialize(msg.getContent()); //序列化
header.setLength(data.length);
out.writeInt(data.length); //写入消息长度
out.writeBytes(data);
}
}

NettyServer

实现NettyServer构建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码@Slf4j
public class NettyServer{
private String serverAddress; //地址
private int serverPort; //端口

public NettyServer(String serverAddress, int serverPort) {
this.serverAddress = serverAddress;
this.serverPort = serverPort;
}

public void startNettyServer() throws Exception {
log.info("begin start Netty Server");
EventLoopGroup bossGroup=new NioEventLoopGroup();
EventLoopGroup workGroup=new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new RpcServerInitializer());
ChannelFuture channelFuture = bootstrap.bind(this.serverAddress, this.serverPort).sync();
log.info("Server started Success on Port:{}", this.serverPort);
channelFuture.channel().closeFuture().sync();
}catch (Exception e){
log.error("Rpc Server Exception",e);
}finally {
workGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}

RpcServerInitializer

1
2
3
4
5
6
7
8
9
10
java复制代码public class RpcServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,12,4,0,0))
.addLast(new RpcDecoder())
.addLast(new RpcEncoder())
.addLast(new RpcServerHandler());
}
}

RpcServerHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码public class RpcServerHandler extends SimpleChannelInboundHandler<RpcProtocol<RpcRequest>> {

@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcProtocol<RpcRequest> msg) throws Exception {
RpcProtocol resProtocol=new RpcProtocol<>();
Header header=msg.getHeader();
header.setReqType(ReqType.RESPONSE.code());
Object result=invoke(msg.getContent());
resProtocol.setHeader(header);
RpcResponse response=new RpcResponse();
response.setData(result);
response.setMsg("success");
resProtocol.setContent(response);

ctx.writeAndFlush(resProtocol);
}

private Object invoke(RpcRequest request){
try {
Class<?> clazz=Class.forName(request.getClassName());
Object bean= SpringBeansManager.getBean(clazz); //获取实例对象(CASE)
Method declaredMethod=clazz.getDeclaredMethod(request.getMethodName(),request.getParameterTypes());
return declaredMethod.invoke(bean,request.getParams());
} catch (ClassNotFoundException | NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
}
}

SpringBeansManager

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Component
public class SpringBeansManager implements ApplicationContextAware {
private static ApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringBeansManager.applicationContext=applicationContext;
}

public static <T> T getBean(Class<T> clazz){
return applicationContext.getBean(clazz);
}
}

需要注意,这个类的构建好之后,需要在netty-rpc-provider模块的main方法中增加compone-scan进行扫描

1
2
3
4
5
6
7
8
9
java复制代码@ComponentScan(basePackages = {"com.example.spring","com.example.service"})  //修改这里
@SpringBootApplication
public class NettyRpcProviderMain {

public static void main(String[] args) throws Exception {
SpringApplication.run(NettyRpcProviderMain.class, args);
new NettyServer("127.0.0.1",8080).startNettyServer(); // 修改这里
}
}

netty-rpc-consumer

接下来开始实现消费端

RpcClientProxy

1
2
3
4
5
6
7
8
9
java复制代码public class RpcClientProxy {

public <T> T clientProxy(final Class<T> interfaceCls,final String host,final int port){
return (T) Proxy.newProxyInstance
(interfaceCls.getClassLoader(),
new Class<?>[]{interfaceCls},
new RpcInvokerProxy(host,port));
}
}

RpcInvokerProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码@Slf4j
public class RpcInvokerProxy implements InvocationHandler {

private String serviceAddress;
private int servicePort;

public RpcInvokerProxy(String serviceAddress, int servicePort) {
this.serviceAddress = serviceAddress;
this.servicePort = servicePort;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("begin invoke target server");
//组装参数
RpcProtocol<RpcRequest> protocol=new RpcProtocol<>();
long requestId= RequestHolder.REQUEST_ID.incrementAndGet();
Header header=new Header(RpcConstant.MAGIC, SerialType.JSON_SERIAL.code(), ReqType.REQUEST.code(),requestId,0);
protocol.setHeader(header);
RpcRequest request=new RpcRequest();
request.setClassName(method.getDeclaringClass().getName());
request.setMethodName(method.getName());
request.setParameterTypes(method.getParameterTypes());
request.setParams(args);
protocol.setContent(request);
//发送请求
NettyClient nettyClient=new NettyClient(serviceAddress,servicePort);
//构建异步数据处理
RpcFuture<RpcResponse> future=new RpcFuture<>(new DefaultPromise<>(new DefaultEventLoop()));
RequestHolder.REQUEST_MAP.put(requestId,future);
nettyClient.sendRequest(protocol);
return future.getPromise().get().getData();
}
}

定义客户端连接

在netty-rpc-protocol这个模块的protocol包路径下,创建NettyClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码@Slf4j
public class NettyClient {
private final Bootstrap bootstrap;
private final EventLoopGroup eventLoopGroup=new NioEventLoopGroup();
private String serviceAddress;
private int servicePort;
public NettyClient(String serviceAddress,int servicePort){
log.info("begin init NettyClient");
bootstrap=new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new RpcClientInitializer());
this.serviceAddress=serviceAddress;
this.servicePort=servicePort;
}

public void sendRequest(RpcProtocol<RpcRequest> protocol) throws InterruptedException {
ChannelFuture future=bootstrap.connect(this.serviceAddress,this.servicePort).sync();
future.addListener(listener->{
if(future.isSuccess()){
log.info("connect rpc server {} success.",this.serviceAddress);
}else{
log.error("connect rpc server {} failed .",this.serviceAddress);
future.cause().printStackTrace();
eventLoopGroup.shutdownGracefully();
}
});
log.info("begin transfer data");
future.channel().writeAndFlush(protocol);
}
}

RpcClientInitializer

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Slf4j
public class RpcClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
log.info("begin initChannel");
ch.pipeline()
.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,12,4,0,0))
.addLast(new LoggingHandler())
.addLast(new RpcEncoder())
.addLast(new RpcDecoder())
.addLast(new RpcClientHandler());
}
}

RpcClientHandler

需要注意,Netty的通信过程是基于入站出站分离的,所以在获取结果时,我们需要借助一个Future对象来完成。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Slf4j
public class RpcClientHandler extends SimpleChannelInboundHandler<RpcProtocol<RpcResponse>> {

@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcProtocol<RpcResponse> msg) throws Exception {
log.info("receive rpc server result");
long requestId=msg.getHeader().getRequestId();
RpcFuture<RpcResponse> future=RequestHolder.REQUEST_MAP.remove(requestId);
future.getPromise().setSuccess(msg.getContent()); //返回结果
}
}

Future的实现

在netty-rpc-protocol模块中添加rpcFuture实现

RpcFuture

1
2
3
4
5
6
7
8
9
10
java复制代码@Data
public class RpcFuture<T> {
//Promise是可写的 Future, Future自身并没有写操作相关的接口,
// Netty通过 Promise对 Future进行扩展,用于设置IO操作的结果
private Promise<T> promise;

public RpcFuture(Promise<T> promise) {
this.promise = promise;
}
}

RequestHolder

保存requestid和future的对应结果

1
2
3
4
5
6
java复制代码public class RequestHolder {

public static final AtomicLong REQUEST_ID=new AtomicLong();

public static final Map<Long,RpcFuture> REQUEST_MAP=new ConcurrentHashMap<>();
}

需要源码的同学,请关注公众号[跟着Mic学架构],回复关键字[rpc],即可获得

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Mic带你学架构!
如果本篇文章对您有帮助,还请帮忙点个关注和赞,您的坚持是我不断创作的动力。欢迎关注同名微信公众号获取更多技术干货!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

dart系列之 创建Library package 简介 L

发表于 2021-11-17

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」

简介

在dart系统中,有pubspec.yaml文件的应用就可以被成为一个package。而Libray package是一类特殊的package,这种包可以被其他的项目所依赖. 也就是通常所说的库。

如果你也想你写的dart程序可以上传到pub.dev上,或者提供给别人使用,则来看看这篇文章吧。

Library package的结构

先看下library package的结构:

1
2
3
4
css复制代码app3
├── lib
│   └── main.dart
└── pubspce.yaml

这是一个最简单的Library package的结构,在root目录下面,我们有一个pubspce.yaml文件。然后还有一个lib目录存放的是library的代码。

一般来说lib下面的库是可以供外部进行引用的。如果是library内部的文件,则可以放到lib/src目录下面,这里面的文件表示是private的,是不应该被别的程序引入的。

如果想要将src中的包导出供外部使用,则可以在lib下面的dart文件中使用export,将需要用到的lib导出。这样其他用户只需要import这个一个文件即可。

export的例子如下:

1
2
3
4
5
6
7
8
9
css复制代码library animation;

export 'src/animation/animation.dart';
export 'src/animation/animation_controller.dart';
export 'src/animation/animations.dart';
export 'src/animation/curves.dart';
export 'src/animation/listener_helpers.dart';
export 'src/animation/tween.dart';
export 'src/animation/tween_sequence.dart';

上面的代码是flutter的animation库。

导入library

怎么使用呢?我们可以使用import语句来导入对应的lib:

1
arduino复制代码import 'package:flutter/animation.dart';

如果是内部文件的导入,则可以使用相对路径。只有在导入外部package的时候才需要加上package:前缀。

条件导入和导出library

因为dart是设计在可以在不同的平台上进行工作,所以一个library在不同的平台可能需要导入或者导出不同的library文件, 这就叫做条件导入和导出。

比如可以通过判断dart库是io库还是html库来选择导出不同的文件:

1
2
3
dart复制代码export 'src/hw_none.dart' // Stub implementation
if (dart.library.io) 'src/hw_io.dart' // dart:io implementation
if (dart.library.html) 'src/hw_html.dart'; // dart:html implementation

上面的意思是,如果在app中能够使用dart:io,那么就导出src/hw_io.dart.

如果能够使用dart:html,那么就导出src/hw_html.dart,否则就导出src/hw_none.dart。

如果是条件导入的话,将export改成import即可。

添加其他有效的文件

因为不同的library有不同的作用,所以通常需要添加一些额外的文件来保证library的有效性和完整性。

为了保证library的有效性,需要添加测试代码,测试代码通常放在test目录中。

如果是创建命令行工具,则需要将对应的工具放到tools目录中。

另外还有 README.md 和 CHANGELOG.md等文件。

library的文档

dart文档可以使用 dartdoc这个工具来生成。dart中的文档格式是以///开头的,如下:

1
2
3
4
csharp复制代码/// The event handler responsible for updating the badge in the UI.
void updateBadge() {
...
}

发布到pub.dev

一个最好共享library的方式就是将其发送到pub.dev上。具体的命令是:pub publish。

总结

以上就是dart中创建library的全部内容。

本文已收录于 www.flydean.com/11-dart-cre…

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

318 最大单词长度乘积 简单位运算模拟题

发表于 2021-11-17

「这是我参与11月更文挑战的第 17 天,活动详情查看:2021最后一次更文挑战」。

题目描述

这是 LeetCode 上的 318. 最大单词长度乘积 ,难度为 中等。

Tag : 「模拟」、「位运算」、「哈希表」

给定一个字符串数组 words,找到 length(word[i]) * length(word[j]) 的最大值,并且这两个单词不含有公共字母。你可以认为每个单词只包含小写字母。如果不存在这样的两个单词,返回 000。

示例 1:

1
2
3
4
5
arduino复制代码输入: ["abcw","baz","foo","bar","xtfn","abcdef"]

输出: 16

解释: 这两个单词为 "abcw", "xtfn"。

示例 2:

1
2
3
4
5
arduino复制代码输入: ["a","ab","abc","d","cd","bcd","abcd"]

输出: 4

解释: 这两个单词为 "ab", "cd"。

示例 3:

1
2
3
4
5
less复制代码输入: ["a","aa","aaa","aaaa"]

输出: 0

解释: 不存在这样的两个单词。

提示:

  • 2 <= words.length <= 1000
  • 1 <= words[i].length <= 1000
  • words[i] 仅包含小写字母

模拟

根据题意进行模拟即可,利用每个 words[i]words[i]words[i] 只有小写字母,且只需要区分两字符是否有字母重复。

我们可以使用一个 int 来代指某个 word[i]word[i]word[i]:低 262626 来代指字母 a-z 是否出现过。

然后对每个「字符对」所对应的两个 int 值执行 & 操作(若两字符无重复字符,则结果为 000),并得出最终答案。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Java复制代码class Solution {
public int maxProduct(String[] words) {
int n = words.length, idx = 0;
int[] masks = new int[n];
for (String w : words) {
int t = 0;
for (int i = 0; i < w.length(); i++) {
int u = w.charAt(i) - 'a';
t |= (1 << u);
}
masks[idx++] = t;
}
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
if ((masks[i] & masks[j]) == 0) ans = Math.max(ans, words[i].length() * words[j].length());
}
}
return ans;
}
}
  • 时间复杂度:令 nnn 为 wordswordswords 数组的长度,转换出 masksmasksmasks 的复杂度为 O(∑i=0i=n−1words[i].length)O(\sum_{i = 0}^{i = n - 1}words[i].length)O(∑i=0i=n−1​words[i].length);得到答案的复杂度为 O(n2)O(n^2)O(n2)。整体复杂度为 O(max⁡(∑i=0i=n−1words[i].length,n2))O(\max(\sum_{i = 0}^{i = n - 1}words[i].length, n^2))O(max(∑i=0i=n−1​words[i].length,n2))
  • 空间复杂度:O(n)O(n)O(n)

优化

不难发现,对于词频相同(maskmaskmask 值相等)的两字符,只需要保留字符长度大的即可,因此我们可以使用「哈希表」代替 masksmasksmasks 数组。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Java复制代码class Solution {
public int maxProduct(String[] words) {
Map<Integer, Integer> map = new HashMap<>();
for (String w : words) {
int t = 0, m = w.length();
for (int i = 0; i < m; i++) {
int u = w.charAt(i) - 'a';
t |= (1 << u);
}
if (!map.containsKey(t) || map.get(t) < m) map.put(t, m);
}
int ans = 0;
for (int a : map.keySet()) {
for (int b : map.keySet()) {
if ((a & b) == 0) ans = Math.max(ans, map.get(a) * map.get(b));
}
}
return ans;
}
}
  • 时间复杂度:令 nnn 为 wordswordswords 数组的长度,得到 mapmapmap 的复杂度为 O(∑i=0i=n−1words[i].length)O(\sum_{i = 0}^{i = n - 1}words[i].length)O(∑i=0i=n−1​words[i].length);得到答案的复杂度为 O(n2)O(n^2)O(n2)。整体复杂度为 O(max⁡(∑i=0i=n−1words[i].length,n2))O(\max(\sum_{i = 0}^{i = n - 1}words[i].length, n^2))O(max(∑i=0i=n−1​words[i].length,n2))
  • 空间复杂度:O(n)O(n)O(n)

最后

这是我们「刷穿 LeetCode」系列文章的第 No.318 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour…

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

操作符这块,你可得把握住(上)

发表于 2021-11-17

这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战」


最近,想复习一下C语言,所以笔者将会在掘金每天更新一篇关于C语言的文章! 各位初学C语言的大一新生,以及想要复习C语言/C++知识的不要错过哦! 夯实基础,慢下来就是快!


1.操作符分类

1
c复制代码算术操作符: + - * / %
1
c复制代码移位操作符: << >>
1
c复制代码位操作符: & | ^
1
c复制代码赋值操作符: =  +=  -=  *=  /=  %=  &=  ^=  & *
1
c复制代码单目操作符: sizeof ! ++ --
1
c复制代码关系操作符: > >= <= < != ==
1
c复制代码逻辑操作符: && ||
1
c复制代码条件操作符:  ?:		唯一的一个三木操作符
1
c复制代码逗号表达式: .
1
c复制代码下标引用,函数调用和结构成员: []  ()  .  ->

/ -除号操作符

对于除号操作符:

1.两边都是整数:执行整数除法,与保存的类型无关

1
2
3
4
5
6
7
8
9
10
11
c复制代码int main()
{
int a = 0;
int b = 0;
int c = 0;
printf("输入两个操作数:->\n"); // 5 2
scanf("%d %d", &a, &b);
c = a / b;
printf("结果为: %d\n", c); // 2
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
c复制代码//若接收类型为double类型
int main()
{
int a = 0;
int b = 0;
double c = 0;
printf("输入两个操作数:->\n"); // 5 2
scanf("%d %d", &a, &b);
c = a / b;
printf("结果为: %lf\n", c); // 2.000000 并不是2.500000
return 0;
}

% - 取模操作符

对于取模操作符:%操作符两边元素只能为整形

1
2
3
c复制代码int a = 10;
int b = 3;
int c = a % b;

1
2
3
c复制代码int a = 10;
double b = 3;
int c = a % b;//err

1
2
3
c复制代码int a = 10;
int b = 3;
double c = a % b; //接收类型可以为其他类型

A % B : 最终得到的值小于B

所以:

rand():产生随机数的函数-通常配套srand()函数:随机数生成器使用

(具体内容看分支与循环猜数字游戏)

​ –>srand(unsigned int)time(NULL) //拿时间戳作为参数

生成的随机数范围:0-32767

所以若要得到0-99的数:rand() %100

若要得到1-100的数:rand()%100 +1


<< >> 左移右移操作符

<< 左移操作符

右移操作符

移动的是二进制位(补码) –只针对整形


左移操作符 <<

左边丢弃,右边补0

左移相当于数值乘2

1
2
3
4
5
6
7
8
c复制代码int c = 4;
c << 1;
printf("%d\n",c); //4还是8?
//4 原因:并没有接收c << 1的值

//若想结果位8
c = c<< 1;
printf("%d\n",c); //8

右移操作符 >>

1
2
c复制代码情形1:逻辑右移:左边用0填充,右边丢弃
情形2:算术右移:左边补符号位,右边丢弃

当前VS2019采用的是算术右移

1
2
3
4
5
6
7
8
9
10
c复制代码int a = 1;
// 00000000 00000000 00000000 00000001 --原码
//正数:原反补相同

//若采取的是逻辑右移
// 00000000 00000000 00000000 00000000 -补码
//最高位为符号位:0,正数 原反补相同
//对应原码为:00000000 00000000 00000000 00000000 -打印结果为0
a = a >> 1;
printf("%d\n",a); //0

1
2
3
c复制代码//err写法
int a = 15;
int b = a >> -1; //C语言标准未被定义的写法

注意:左移/右移操作数只能为整形,不能为浮点数

1
2
c复制代码float c = 4,5f;
c >> 1; //err

补码为全1 -> %d打印 表示-1

补码求补码 ->原码


关于 & | ^

只能用在整形数据(正数,负数都可以)!

& 按位与

1
2
3
4
5
c复制代码 1001
&1111
-----
1001
//对应比特位进行按位与运算 有0则为0 两个比特位都为1,结果才为1

| 按位或

1
2
3
4
5
c复制代码 1001
|1111
-----
1111
//对应比特位进行按位与运算 有1则为1 两个比特位都为0,结果才为0

^ 按位异或

1
2
3
4
5
c复制代码 1001
^1111
-----
0110
//对应比特位进行按位异或运算 对应比特位相同为0 不同为1

使特定位翻转,即异或上该特定位为1,其它位为0的二进制序列

1
2
3
4
5
6
c复制代码如: X: 1100 0011 使倒数第三个二进制位翻转
使1100 0011异或上0000 0100
==> 1100 0011
^0000 0100
----------
1100 0111 ==>这样的话就使X的倒数第三位翻转了
1
2
3
4
5
6
7
c复制代码使特定位翻转:
对应X要翻转的比特位,该数的对应位为1,其他位为0,次数与X对应位异或即可
如:X = 10101110 使X的低4字节位翻转,
X ^ 0000 1111即可
==>1010 1110
^0000 1111
==>1010 0001 ->x的低4位翻转了

2.如何得到二进制序列最后一位比特位是1还是0

首先,整形在内存中以补码形式存储

方法:只需要让该位按位与上1,即可得知最后一位比特位是0还是1


若最后一位比特位为1:结果为1 否则为0

1的二进制序列: 00000000 00000000 00000000 00000001

符号位为0,正数:原反补相同

其他位比特为0,所以和比特位相于的结果为0

1
2
3
4
5
6
c复制代码int a  =15;
// 00000000 00000000 00000000 00001111 ->a的补码
//&00000000 00000000 00000000 00000001 ->1的补码
--------------------------------------
// 00000000 00000000 00000000 00000001 ->结果为1
//即a的最后一位比特位为1
1
2
3
4
5
6
7
8
c复制代码int main()
{
int a = 0;
int b = 0;
scanf("%d",&b);
a = b &1;
printf("%d补码的最后一位比特位是%d",b,a);
}

今天就先到这吧~感谢你能看到这里!希望对你有所帮助!欢迎老铁们点个关注订阅这个专题! 同时欢迎大佬们批评指正!


本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…313314315…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%