Redis
中的常用的链表数据结构Redis
低版本(3.2之前)中list数据结构底层就是使用链表来进行串联数据的!Redis
中在操作List数据结构时的结构图!在redis
中并不是仅仅使用这一种双向的链表结构.关于ziplist等其他结构我们这里暂时不讨论。typedef struct listNode { struct listNode *prev; struct listNode *next; void *value;} listNode;typedef struct listIter { listNode *next; int direction;} listIter;typedef struct list { listNode *head; listNode *tail; void *(*dup)(void *ptr); void (*free)(void *ptr); int (*match)(void *ptr, void *key); unsigned long len;} list;
函数 | 作用 |
---|---|
dup | 复制链表节点值 |
free | 释放链表节点值 |
match | 比对链表节点值与指定值是否想的相等 |
位置 | 作用 |
---|---|
zlbytes | ziplist字节长度;长度最长为(2^32)-1 |
zltail | 整个ziplist偏移量;四个字节 |
zllen | ziplist中存储的元素个数;两个字节 |
entryX | ziplist中元素 |
zlend | 结束位 。固定值0xFF=255 |
typedef struct zlentry { unsigned int prevrawlensize, prevrawlen; unsigned int lensize, len; unsigned int headersize; unsigned char encoding; unsigned char *p;} zlentry;
欢迎点个赞呗
]]>在我们讲解list结构的时候提到了一种特殊的结构ziplist ,俗称压缩列表。同时他也是hash结构和list结构采用的底层结构之一。他的出现是为了节省内存的一种结构。
当一个list结构的数据中只包含少量列表项且里面元素是小整数或者是短的字符串时redis底层就会使用ziplist来存储数据
字段 | 类型 | 长度 | 作用 |
---|---|---|---|
zlbytes | unit32_t | 四字节,32位 | 整个ziplist占用字节数。 |
zltail | unit32_t | 四字节,32位 | 尾结点距离起始位置偏移量。这里需要尾结点句柄距离头部偏移量 |
zllen | unit16_t | 二字节,16位 | 节点个数 。 16位表示最大值65535 。 当元素超过65535时我们只能遍历获取节点个数 |
entry … | entry | 动态 | |
zlend | unit8_t | 一字节,8位 | 固定值0XFF 。 用于表示标记位 |
编码 | 长度(字节) | 值 | ||||
---|---|---|---|---|---|---|
00aaaaaa | 1 | 长度<=63 字符串 | ||||
01pppppp\ | qqqqqqqq | 2 | 长度<=16383 字符串 | |||
10xxxxxx\ | qqqqqqqq\ | rrrrrrrr\ | ssssssss\ | tttttttt | 5 | 长度<=4294967295 字符串 |
11000000 | 1 | int16 | ||||
11010000 | 1 | int32 | ||||
11100000 | 1 | int64 | ||||
11110000 | 1 | 24位有符号整数 | ||||
11111110 | 1 | 8位有符号整数 | ||||
1111xxxx (xxxx的取值范围【0000 , 1101)) | 1 | 0~12 | ||||
11111111 | ziplist结束位 |
前两位11表示整数;00表示字节长的字符串;01表示字节字符串;10表示5字节字符串
计算长度就是encoding去除前两位后表示的数字就是属性的长度。
在上面我们知道previous_entry是用于标记前一节点的长度,而且当时我们也说了redis为了节省内存会先尝试用1字节来表示当长度超出254时才会扩容为4字节长度。这就意味着我们每个entry节点长度都是动态的,换言之我们每个entry都会受到前一节点的影响
比如说现在我们的entry的长度都在250~254之间。如下图
求个赞,谢谢大爷!!!
]]>整数集合相信有的同学没有听说过,因为redis对外提供的只有封装的五大对象!而我们本系列主旨是学习redis内部结构。内部结构是redis五大结构重要支撑!
前面我们分别从redis内部结构分析了redis的List、Hash、Zset三种数据结构了。今天我们再来分析set数据结构内部是如何存储的
我们在通过object encoding key
来查看下两个集合的底层数据结构,发现一个是hashtable 一个是intset 。这也验证了我们上面对set基本结构的描述。
在redis中对外提供五大类型实际上都是redis的一个抽象对象叫做redisobject。在内部映射了我们redis内部的数据结构
你可以单纯的认为只要是数字就会使用intset结构来存储,我恐怕要给你当头一棒了。实际上并不是这样
需要同时满足以下两个条件:
$$
-2^{7} \sim 2^{7}-1 \
即 \
-128 \sim 127
$$
sadd juejin -123sadd juejin -6sadd juejin 12sadd juejin 56sadd juejin 321
所以当前五个元素占contents的长度是16*5=80 ;
注意set在存储int类型数据时,内部是按照从小到大的顺序存储的。
Redis
中Hash结构。在里面我们梳理了字典这个重要的内部结构并分析了hash结构rehash的流程从而解释了为什么redis
单线程还是那么快Redis
五大天王中的zset
数据结构 ; zset
是有序不重复集合其内部元素唯一且是有序的,他的排序标准是根据其内部score维度进行排序的。zskiplist
。zskiplistNode
这个重要的节点说明。实际上他就是我们右侧那个链表中节点。换句话说链表中每个点就是zskiplistNode 。首先根据level的定义我们可以总结如下几点关于level的特性。
①、一个节点如果在level[i]中,那么他一定在level[i]以下的层中
点赞、关注不迷路哦
]]>mongo
中,查询是通过MongoTemplate
来实现数据的查询已验收
、未验收
实际存储的是1、2@Datapublic class DangersBook implements Serializable { @TimeSequence @Ignore @Id @Query private Long id; @DangersNo @Ignore @Query @LikeQuery private String dangersNo; @Ids private String instance_id; @DangersType @Ids private String app_id; @Ids @Query @DangersEntId @OrQuery private String entId; private String created_by; @Query @Ids private String zuoyeleixing; @Date @Query private String pizhunkaishi; @Date @Query private String pizhunjieshu; @UpdateItem(value = "已验收") @Query @OrQuery private String yanshouStatus; @Query @Date @UpdateItem(value = "") private String finishTime; private String jianweixiuxukezheng; private String zuoyedengji; @Query @LikeQuery private String zuoyeneirong; /** * @Description 用于判断是否应该生成编号 */ @Ignore @Query private String createDangerNo; private String laowubanzu; @Query private String test; @Query private String interrupt; @Query @OrQuery private String year;}
DangersBook
中的所有属性!这是我们对反射的第一处运用!实际上在项目中凡是需要写活属性的获取的都是通过反射进行获取结合注解进行属性过滤!TimeSequence annotation = declaredField.getAnnotation(TimeSequence.class);if (annotation!=null) { //雪花算法 long generatorId = DistributedIDUtils.getInstance().getGeneratorId(1l, 2l, 3l); declaredField.setAccessible(true); declaredField.set(object, generatorId);}
TimeSequence
之后我们就可以通过雪花算法生成id然后通过属性将生成的id赋值进去!Ignore
就是这个作用Ignore ignore = declaredField.getAnnotation(Ignore.class);if (ignore != null) { continue;}
Query
基础上进行扩展的com.ay.sdksm.custom.components.expose.annation.Query queryField = declaredField.getAnnotation(com.ay.sdksm.custom.components.expose.annation.Query.class);if (queryField != null) { //需要添加查询条件 try { declaredField.setAccessible(true); Object o = declaredField.get(dangersBook); if (null != o&&!"".equals(o.toString())) { String svalue = o.toString(); Date dateAnnotation = declaredField.getAnnotation(Date.class); LikeQuery likeQuery = declaredField.getAnnotation(LikeQuery.class); if (dateAnnotation != null) { String dateValue = svalue; if (StringUtils.isEmpty(dateValue)) { continue; } String[] dateSplit = dateValue.split(","); String start = dateSplit[0]; String end = start; if (dateSplit.length >= 2) { end = dateSplit[1]; } criteria.and(declaredField.getName()).gte(start).lte(end); } else if(likeQuery != null){ Pattern pattern = Pattern.compile(String.format(".*%s.*", svalue), Pattern.CASE_INSENSITIVE); criteria.and(declaredField.getName()).regex(pattern); }else { OrQuery orQuery = declaredField.getAnnotation(OrQuery.class); if (null != orQuery) { String[] split = svalue.split(","); String start = split[0]; String end = split[0]; if (split.length > 1) { end = split[1]; } criteria.and(declaredField.getName()).in(start, end); } else { if (svalue.contains("$ne")&& JsonUtils.getInstance().isJson(svalue)) { JSONObject jsonObject = JSONObject.parseObject(svalue); String $ne = jsonObject.getString("$ne"); criteria.and(declaredField.getName()).ne($ne); }else if(svalue.contains("$like")&& JsonUtils.getInstance().isJson(svalue)){ //模糊查询 JSONObject jsonObject = JSONObject.parseObject(svalue); String $like = jsonObject.getString("$like"); criteria.and(declaredField.getName()).is(String.format("/%s/",$like)); } else { Class<?> type = declaredField.getType(); if (type == Long.class) { criteria.and(declaredField.getName()).is(Long.valueOf(svalue)); } else { criteria.and(declaredField.getName()).is(svalue); } } } } } } catch (IllegalAccessException e) { e.printStackTrace(); }}
Query
注解表示需要对字段进行条件组装。当遇到Date我们就需要将条件值进行定制处理我们约定的条件值是已逗号间隔的时间段。这样我们就可以动态的支持时间点、时间段的查询。LikeQuery
表示是模糊查询。DangersNo annotation = declaredField.getAnnotation(DangersNo.class);if (annotation != null) { //根据业务生成指定格式的数据ID;这里比较复杂。代码省略。。。。。。}
点赞,XDM
]]>
redis
和数据相比除了他们的结构型颠覆以外!还有他们存储位置也是不相同。传统数据库将数据存储在硬盘上每次数据操作都需要IO
而Redis
是将数据存储在内存上的。这里稍微解释下IO是啥意思。IO就是输入流输出流方式将数据在硬盘和内存之间进行交互!而redis
直接在内存上就剩下了IO
操作。这也是redis
快的原因之一吧
redis
又将数据存储在内存上那么redis
肯定不能肆无忌惮的进行存储 。这就需要redis
和开发者们作出相应的优化redis
在配置文件(redis.conf
)中通过maxmemory
参数指定redis
设置整个对内存的支配大小!redis
中填值的时候根据自己的需求设置相应的key过期时间。这样不必要的数据就会被redis
过期驱逐策略清楚。从而节省内存供别人使用本文凌驾于redis
基础之上,这里笔者默认大家都已经安装了redis
. 并实际使用过redis
maxmemory
指定大小。在redis.conf
配置文件中可以直接指定。他的单位时byte。redis
通过config
命令来设置redis.conf
中的配置。redis
本身角度出发设置了内存限制,这样不用担心他们吞噬系统内存!下面就需要我们开发者设计角度约束自己了。expire key time
设置过期时间默认单位时S 。
然后通过ttl 命令可以查看剩余过期时间
ttl
能够观察到剩余时间在不断的减少!当减少到0的时候就被给驱逐策略驱逐!注意这里说的是驱逐策略驱逐并不是意味着立马被删除del key
直接将key删除了那么该key对应的过期自然也就不存在了!这种情况笔者也算作是更新过期时间set
getset
等命令重新设置key、value方式会覆盖过期时间 , 直接被覆盖成-1set
、 getset
包括del
严格意义是覆盖过期时间。真正做到更新过期时间的还是expire .在expire是已最新为准的!append
、incr
等命令是改变值这种命令是不会影响到原来的过期设置的redis
最大内存设置为1MB
, 设置大小随便最好能小点。然后我们通过Java
小程序不断向redis
中填充。最终当内存不够使用时就会报错redis
拒绝了!不仅仅是新增的被拒绝,就算此时我们想改变已经在redis
中的key的值也是不可用的public static void main(String[] args) { Jedis jedis = new Jedis("39.102.60.114", 6379); jedis.auth("Qq025025"); Integer index = 1; while (true) { String uuid = UUID.randomUUID().toString(); jedis.set(index.toString(), uuid); System.out.println(index++); }}
OOM command not allowed when used memory > maxmemory
。代码中打印和redis
键个数一致;说明我们默认的淘汰策略是直接拒绝总结下来就是:当redis
内存被使用满了后,任何的写操作都会被拒绝!
当没有足够内存时难道就这么直接拒绝吗?上面也提到了需要我们程序员自己根据需求设置键过期已释放内存供其他有需要的key使用!那么设置了过期key之后这些key又是怎么被清除的呢? 这就牵扯出我们的淘汰策略
当内存告警时
redis
会将近期很少使用且设置了过期时间的key剔除出去,即使该key还没有到过期时间。如果没有符合的key也就是执行之后内存仍然不足时将会和默认淘汰策略noeviction
抛出一样的错误OOM command not allowed when used memory > maxmemory
redis.conf
中配置我们最近很少使用策略. maxmemory-policy volatile-lru
。 然后重启我们的redis
服务 。重启之后flushall
清空所有数据,我们在通过上面的Java
程序重新生成下数据!public static void main(String[] args) { Jedis jedis = new Jedis("39.102.60.114", 6379); jedis.auth("Qq025025"); Integer index = 1; while (true) { String uuid = UUID.randomUUID().toString(); if (index < 100) { jedis.setex(index.toString(),360, uuid); } else { jedis.set(index.toString(), uuid); } System.out.println(index++); }}
redis
库中的key数量!就是因为我们为前100设置了过期时间。当内存不足时redis
就会将当前设置了过期时间的key中最近最少使用的key进行剔除!所以我们计数器会大于键数量。因为有部分键被清除了!我们获取前100的key都是null , 说明被删除了! 那么为什么本次计数器不是比上次多100 。 那是因为我们每次存储进来的是uuid, 所占长度都不是固定的。还有本身淘汰策略也是占用内存的noeviction:拒绝写请求,正常提供读请求,这样可以保证已有数据不会丢失(默认策略);2. volatile-lru:尝试淘汰设置了过期时间的key,虽少使用的key被淘汰,没有设置过期时间的key不会淘汰;3. volatile-ttl:跟volatile-lru几乎一样,但是他是使用的key的ttl值进行比较,最先淘汰ttl最小的key;4. volatile-random:其他同上,唯一就是他通过很随意的方式随机选择淘汰key集合中的key;5. allkeys-lru:区别于volatile-lru的地方就是淘汰目标是全部key,没设置过期时间的key也不能幸免;6. allkeys-random:这种方式同上,随机的淘汰所有的key。
redis
。 仅仅这两方面还没有完全高效使用内存!淘汰策略是濒临内存不足时触发。那么当设置了过期时间的键真正到了过期时间而此时内存尚够使用?这种场景是不是需要将过期键删除呢?redis
是单线程,那么在键过期的时候如何不影响对外服务的同时清除过期键呢?答案是【不行】。严格意义是无法解决的因为单线程同时只能做一件事!既然无法解决那么我们可以达到一种协调状态!如果同一时刻出现一个过期键那么清除键很快这时候阻塞外部服务的时间很短可能毫秒级设置纳秒级!redis
如何应对同一时间过多数据过期的场景,他的删除过期键的方法略有不同!key
需要设置一个定时器进行跟踪。redis
这里笔者猜测应该是启用另外线程来进行定时跟踪!这里有清除的还请帮忙解答下?key
很多的时候!我们的CPU就需要不断的执行这些定时器从而导致CPU资源紧张。最终会影响到redis
服务的性能redis
会每隔100ms
执行一次定时器,定时器的任务就是随机抽取20个设置过期的key
。 判断是否进行清除。上面图示中说明中写错了不是10S , 而是每隔100ms
。请原谅我的粗心!!!25ms
。 也就是说对于客户端来说服务端的延迟不会超过25ms
。 redis
并不急着去清除这些数据,而是等到该key被再次请求时进行删除!这样在最终效果上是没有问题的。redis
中。redis
内部是使用惰性清除和定期清除两种方式结合使用,最终保重CPU和内存之间的一种平衡!redis
给我们开发者提供的功能。我们可以根据自己的业务需求合理的设置键的过期时间,从而保证内存的高可用redis.conf
中我们可以设置 hz 10
代表1S
中平均执行几次这也是我们上面所说的100MS
的由来。但是仅仅定期删除会产生遗漏数据所以我们还需要加上惰性清除,最终保证对客户端来说数据是准确实时清除的。redis
服务中还会堆积很多过期的无效key 。这个时候如果内存不够用了的话那又该怎么办呢?这时候我们需要设置淘汰策略比如果volatile-lru
. 就会将最近最少使用的设置过期key进行清除从而保证尽可能的接收更多的有效数据!redis
常见的灾难场景,我们下章节继续分析如何产生的、并且如何进行修复进行展开讨论。运用你所掌握的数据结构,设计和实现一个
LRU
(最近最少使用) 缓存机制 。
实现LRUCache
类:
LRUCache(int capacity)
以正整数作为容量 capacity 初始化LRU
缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。进阶:你是否可以在 O(1) 时间复杂度内完成这两种操作?
LinkedHashMap
LinkedHashMap
不熟悉的朋友们可以简单的将它理解成HashMap
。 下图展示了HashMap
的存储结构LinkedHashMap
只是多了一条链表串起里面的元素LinkedHashMap
是按照顺序存储的。但是LinkedHahsMap
也无法做到按照使用频率进行排序啊?大家都知道他是按照添加顺序排序的!!!LinkedHashMap
改造LinkedHashMap
的确无法满足情况,但是我们稍微看下源码能够发现在put之后都会执行下afterNodeInsertion
这个方法。这也是HashMap
留给LinkedHashMap
做的扩展!removeNode
就是将最前面的数据。想要进入这个方法就需要removeEldestEntry
判断。LinkedHashMap
默认是false . 所以我们只需要重写他就行了。但是还是在get值的时候如何保值在最后面呢?我们仔细看下源码就能够发现在get
中有这个一个方法afterNodeAccess
。他的作用就是将get的元素移位值后面。正好符合我们LRU
的策略特征LinkedHashMap
就非常容易的实现了LRU策略!但是本题的意思是想考察我们自己是如何实现的,而不是巧妙对现有的工具改造的!不过上面对LinkedHashMap
的确改造的很巧这是不可否认的!下面我们就尝试自己来实现下这种方式!
首先我们需要确定需要用到Hash结合链表来实现。Hash我们自然使用HashMap
来存储数据为的就是方便定位数据。定位到数据就需要操作链表将数据实时移位值链表尾部,每次淘汰是将链表首位移除既可。为了方便我们操作链表这里的链表肯定是双链表的!
preNode
,nextNode
分别指向前后节点方法名 | 作用 |
---|---|
addToTail | 将节点添加值链表尾部 |
moveToTail | 将已经存在于链表中的节点移动到链表的尾部 |
removeHeadNode | 删除链表中第一个节点,注意是边界节点后第一个节点 |
给你一个奇怪的打印机,它有如下两个特殊的打印规则:
每一次操作时,打印机会用同一种颜色打印一个矩形的形状,每次打印会覆盖矩形对应格子里原本的颜色。
一旦矩形根据上面的规则使用了一种颜色,那么 相同的颜色不能再被使用 。
给你一个初始没有颜色的 m x n 的矩形targetGrid
,其中targetGrid[row
][col] 是位置 (row, col) 的颜色。如果你能按照上述规则打印出矩形
targetGrid
,请你返回 true ,否则返回 false 。
只有保持这个维度才能确保外部被打乱的颜色可以被打印机打印。因为打印机的两个特性:一次打印矩形+一个颜色只能用一次
换句话说不通颜色我们可以理解成不同卡片叠加在一起产生的效果。最底层的就是我们最外层的颜色。只有这样最小的最上层才会被重新渲染。
m*n
矩阵进行判断!既然是判断是否满足这个奇葩的打印机那就稍微简单一点。for (Map.Entry<Integer, Direction> entry : entries) { Integer key = entry.getKey(); if (sameColorAndPrintMark(key, entry.getValue(), targetGrid)) { value=key; break; }}
sameColorAndPrintMark
方法。该方法是判断区间内是否颜色相同并进行打印标记的。因为颜色取值是[1,60]。所以我们这里使用0来标记已经被其他色块打印过 。然后在判断是否是同一色块时过滤掉已经被渲染色块在进行判断如果不通过则真的无法通过了。private boolean sameColorAndPrintMark(Integer key, Direction direction, int[][] targetGrid) { for (int i = direction.top; i <= direction.bottom; i++) { for (int j = direction.left; j <= direction.right; j++) { if(targetGrid[i][j]!=0&&targetGrid[i][j]!=key) return false; } } for (int i = direction.top; i <= direction.bottom; i++) { for (int j = direction.left; j <= direction.right; j++) { targetGrid[i][j] = 0; } } return true;}
if (value == -1) { return false;} else { //剔除 colorMap.remove(value);}
class Direction{ int left = 61; int right = -1; int top = 61; int bottom = -1;}public boolean isPrintable(int[][] targetGrid) { int[] values=new int[61]; int n=targetGrid.length, m=targetGrid[0].length; Map<Integer,Direction> colorMap = new HashMap<>(); for(int i=0; i<n; i++){ for(int j=0; j<m; j++){ int val=targetGrid[i][j]; Direction direction = null; if (colorMap.containsKey(val)) { direction = colorMap.get(val); } else { direction = new Direction(); colorMap.put(val,direction); } direction.left = Math.min(direction.left, j); direction.right = Math.max(direction.right, j); direction.top = Math.min(direction.top, i); direction.bottom = Math.max(direction.bottom, i); } } while (!isAllPrint(targetGrid)) { int value=-1; Set<Map.Entry<Integer, Direction>> entries = colorMap.entrySet(); for (Map.Entry<Integer, Direction> entry : entries) { Integer key = entry.getKey(); if (sameColorAndPrintMark(key, entry.getValue(), targetGrid)) { value=key; break; } } if (value == -1) { return false; } else { //剔除 colorMap.remove(value); } } return true;}private boolean isAllPrint(int[][] targetGrid) { for (int i = 0; i < targetGrid.length; i++) { for (int j = 0; j < targetGrid[i].length; j++) { if (targetGrid[i][j]!=0) { return false; } } } return true;}private boolean sameColorAndPrintMark(Integer key, Direction direction, int[][] targetGrid) { for (int i = direction.top; i <= direction.bottom; i++) { for (int j = direction.left; j <= direction.right; j++) { if(targetGrid[i][j]!=0&&targetGrid[i][j]!=key) return false; } } for (int i = direction.top; i <= direction.bottom; i++) { for (int j = direction.left; j <= direction.right; j++) { targetGrid[i][j] = 0; } } return true;}
但是需要多少次我们也无法确定,这时候我们就一直渲染!但是不能一直渲染所以我们每次渲染后需要对是否需要继续下去进行判定
当然笔者这里也不是一番风顺的,提交过程也是不断的试错调试。这里我只是想告诉读者们刷题需要不断努力,不要因为错误而放弃
这里点个赞、关个注呗!持续贡献原创文章!如果你觉得那个算法有意思,下方告诉我,我去试试能否攻克!!!
]]>给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。
最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。
你可以假设除了整数 0 之外,这个整数不会以零开头。
java
中基础类型中的int、long等类型的取值范围public int[] plusOne(int[] digits) { int total = 0; for (int i = 0; i <digits.length ; i++) { total = total * 10 + digits[i]; } total += 1; for (int i = digits.length - 1; i >= 0; i--) { digits[i]=total%10; total = total / 10; } if (total != 0) { digits = new int[digits.length + 1]; digits[0] = 1; } return digits;}
2147483647
即2^31。那么就会出现数组转成数组超出范围的情况。那么换成long可以吗?同样的分析long类型64位也无法满足100的长度。所以我们这里的思路在本题中是无法通过检验的。c=a+b
。第一步我们开始从末尾也就是4开始进行加1操作!得到的结果是5.因为5!=10。所以没有产生进位,所以在4之前的数据都不会发生变化。所以我们直接将5赋予数组末尾然后直接返回数组
如果是9999,我们在最后一位进行加1,结果是10这个时候产生进位我们就需要到第三位进行加1,同样会产生进位那么我们就需要一直重复下去。最终数组会被更新为0000.
上面我们分析了进位只可能是1 , 所以在所有位都发生了进位后原数组就会变成0 , 我们只需要在数组头部添加一个1元素即可
public int[] plusOne(int[] digits) { for (int i = digits.length - 1; i >= 0; i--) { digits[i]++; digits[i] = digits[i] % 10; //对10取余之后如果是0说明发生进位,否则没有进位直接结束 if (digits[i] != 0) return digits; } //说明当前数组全部发生进位类似999数字!此时需要扩展数组并且第一位为进位1 digits = new int[digits.length + 1]; digits[0] = 1; return digits;}
点赞呗!
]]>xxx
年份中有5个指标考核。每个指标考核维度不一样,对于指标1考核目标是一天施工一次,然后对一个月进行汇总考核有点类似于上班打卡的形式。而他打卡的方式就是后方的施工按钮。对于指标1他只需要每天点击施工填写响应的数据即可!关于施工后填写数据就是一份表单数据这个是非常简单的。AbstrachTimeStrategy
该类主要责任就是对数据进行分析,接收两个参数分别是Map
, bean
。前者是用户月份详情数据,后者是根据频率相关的解析数据。在每个策略中针对单一的场景进行具体分析,每个类具有了单一的职责这样将他们进行分离,比如说上面PerHalfSomeTimesStrategy
出现的bug那么我们只需要对他进行修复,其他的策略是不会受影响的。PerDaySomeTimesStrategy
策略对应的一天N次的频率。我们在里面只需要获取到指定月份的施工记录并对这些记录按天分组,最终看看每天的完成情况是否满足我们N次的要求就可以了。而不需要去考虑季度、年的频率问题static { strategyMap.put("month", new PerMonthSomeTimesStrategy()); strategyMap.put("halfMonth", new PerHalfMonthSomeTimesStrategy()); strategyMap.put("year", new PerYearSomeTimesStrategy()); strategyMap.put("day", new PerDaySomeTimesStrategy()); strategyMap.put("squash", new PerSquarshSomeTimesStrategy()); strategyMap.put("halfyear", new PerHalfYearSomeTimesStrategy());}
TimeStrategy strategy = ContextTimeStrategyFactory.getInstance().getStrategy(cycle.getCycleName());
XD
改出问题!想一想当我将内置的功能如果进行maven进行分模块开发那么其他人不论怎么修改都不会影响到我们已有的6中策略了。设计模式真的是我们程序的救星啊!InsertDataHandlerStrategyImpl
算法去解析就行了。每种策略计算出数据之后根据策略的特性在原有基础上对完成情况进行加减就可以了。
封装好方法之后就开始找负责【施工记录】的同学麻烦他们在响应的方法上添加策略的调度!
而且每次需求变动基本上也是对自己当初定下的骨架进行不断的功能完善的。并没有出现因为需求变动导致骨架重新设计的场景。这也足够说明当初选择策略模式的正确性
策略模式其实就是对算法的一种抽象!策略模式往往需要结合工厂模式。每种策略实现的功能是一样的像上述的场景每种策略都是对考核月数据进行考核只不过每个策略的考核依据不同而已。而策略本身值关注算法的实现而不考虑调用。所以借助于工厂来进行维护策略,方便第三方进行策略的调度!
点赞、关注、产生共鸣!!!
]]>有多少次因为配置文件忘记修改导致重新发布
有多少次因为无法实时修改配置导致重新发布
有多少次同一个配置在不同项目需要重复修改
有多少次因为配置导致项目启动失败!!!
springcloud
为我们提供一种解决方案—Springcloud Config
它为分布式微服务提供了集中化的外部配置支持,配置服务器为微服务下所有环境提供配置中心Springcloud Config
分为服务端和客户端、服务端就是本节介绍的对象。而客户端就是嵌入在各个微服务中和服务端进行交互的从而实现配置的动态获取framework-root
框架来实现我们config中心,首先继承framework-root
然后在pom中添加如下坐标<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId></dependency>
server: port: 8070spring: application: name: config-server cloud.config.server.git: uri: https://gitee.com/zxhTom/spring-cloud-demo searchPaths: helloworldconfig
@SpringBootApplication@EnableConfigServerpublic class ConfigApplication { public static void main(String[] args) { SpringApplication.run(ConfigApplication.class,args); }}
springboot
的开箱即用的强大之处http://localhost:8070/master/config-server-dev.properties
就会将https://gitee.com/zxhTom/spring-cloud-demo
项目下master分支下的helloworldconfig
文件夹下的config-server-dev.properties
文件读取出来!config
仓库后续会有变动。最终读者的演示情况和笔者这里略有不同!!!http://localhost:8070/config-server-dev.yml
。那么config的代理访问肯定是按照一定规律来的。我们访问官网,官网已经帮我们整理好了/{application}/{profile}[/{label}]/{application}-{profile}.yml/{label}/{application}-{profile}.yml/{application}-{profile}.properties/{label}/{application}-{profile}.properties
resultful
、yml
、properties
。当我们的接口满足其中一种格式的时候就会被config
解析出来并有对应变量管理。http://localhost:8070/config/server-dev
.resultful
结构的,那么就只有一种匹配方式得出application=config;profile=server-dev,label=null
。label是可填的默认是master。version
字段,这个笔者猜测是git commitId
,因为我发现和提交记录一样。 而对应的配置文件就是propertySources
里的文件。细心的朋友一定会发现这里为什么是个数组呢?这里笔者在官网上没有找到说明但是经过测试笔者这里整理出springcloud config
映射规则//后缀包括两种 。 回去找{label}分支下如下格式的文件{application}/{profile}.[properties|yml]{application}.[properties|yml]
resultful
差不多,只不过他返回的信息时精简版的。只返回配置文件中内容的并集。这里需要注意相同内容取前者,那么谁先谁后呢?这就需要我们resultful
格式接口告诉我们了。http://localhost:8070/config-server.properties
。 然后通过resultful
风格来确定是来源哪里.这里在强调下上面hello为什么是yml
。还记得上面我提到在这么多文件中如果存在相同的配置会优先去首位的。这是什么意思呢?resultful
可以看出来会读取三个文件的配置分别是config-server.properties
、config-server.yml
、config.properties
。 resultful
接口我们可以看出config-server.yml
排在config.properties
前面,所以我们通过文件后缀方式访问到的数据配置hello=yml
。config
存在两个角色,config
中心是用来统一为微服务提供服务的,剩下的就是嵌入在微服务中的。在配置微服务的config
客户端之前我们先来梳理下springboot
的一个注意点。springboot
的配置文件除了在加载顺序有不同之外,还有一点是文件名的区别。在springboot
中其实存在两种配置文件名称;我们常用的是application开头的配置文件(application.yml
和application.properties
)。springcloud
程序会创建一个bootstrap
上下文同时他也是application上下文的父类!它负责从外部源加载配置属性,并解密本地外部配置文件中的属性。这两个上下文共享一个Environment,它是任何Spring应用程序的外部属性的来源。在springcloud
中bootstrap类型的配置文件优先级最高所以不需要担心会被本地的配置所覆盖。config-server
中心的配置数据我们就需要在bootstrap
配置文件中配置。zxhtom: hello-zxhtomspring: cloud: config: label: master name: config-server profile: dev //这里和config-server解析不一样的是,他将访问master分支下的config-server-dev.yml或者properties文件 uri: http://localhost:8070
zxhtom: hello-zxhtom2server: port: 80 tomcat: max-threads: 10
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId></dependency>
pom
包这里大家应该都没有问题,其次我们在application.yml
和bootstrap.yml
两个配置文件中配置相同的东西。这个时候在bootstrap中不配置config
东西。此时我们访问zxhtom
参数得到的结果是application中的。当我们将config
配置加进来之后我们访问到的是git远程仓库的东西。关于演示笔者这里就不演示了。因为上面配置完成之后我们只需要写个接口获取参数就可以了。但是存在一个小瑕疵,当我们远程仓库配置修改后我们的服务也需要跟着修改!这好坑啊,感情玩了半天我还在原地打转啊。除了解决多模块相同配置重复修改的问题,重启的问题还是没能解决。难道我们就只能如此了吗?
上面我们已经实现config-server
来读取远程仓库配置了。也实现了客户端通过config-server
读取远程配置了。但是当我们修改git远程仓库上配置时,我们的config-server
会实时的修改配置值,客户端确无法实时更新!解决办法就是重启。
actuator
模块,这个我们在讲解hystrix
模块的时候在父项目root中引入了。当时笔者一直出了在高版本中actuator中需要加入actuator前缀。@RefreshScope
。 记住这里一定要在这里加哦!!!order
模块。启动之后通过http://localhost/order/config/getConfig
获取zxhtom这个值。actuator
实现了动态刷新,但是这个动态刷新并不是自动刷新还是需要我们认为参与。实际项目生产使用中会有很多个微服务充电config-client角色。那么我们每次更新git仓库内容时是不是需要诶个调用接口呢?这显然是不行的。我也说了存在问题才能优化。那么我们该如何解决config-server
中我们通过spring.cloud.config.server.git.uri
中指定git远程仓库。如果我们在内网环境开发而且内网中我们没有自己搭建git服务呢。我们可以配置本地地址也可以实现读取指定外部仓库的。spring.cloud.config.server.git.uri=file://xxxxxx/repository
spring.cloud.config.server.git: uri: https://gitee.com/zxhTom/spring-cloud-demo searchPaths: helloworldconfig repos: dev: pattern: dev/* uri: file:///D:\test\repository\spring-cloud-demo searchPaths: helloworldconfig
spring.cloud.config.server.git.uri
是默认的仓库配置。然后根据repos
来进行多仓库的配置。repos
下跟了多少个就说明是多少个环境配置。比如我们上面的配置repos
下只有dev
一个配置,这个dev
就是我们用于dev
的环境。他的匹配模式是任何已dev
开头的都将使用dev
这个配置的仓库来进行我们上面匹配规则分析。如果你的公司没有单独部署git。如果你使用的就是github
这种公网性质。那么将我们项目中的配置放在这种地方是不是有点不安全呢?你的所有的服务的密码都被公开了。这样是极度不安全。那么我们要么自己单独部署git。要么将配置文件这个项目设置成私有
项目配置成私有我们config-server所在的服务可以通过ssh方式进行配置项目uri 。
spring: cloud: config: server: git: username: xxxx password: xxxx
我们也可以通过如上配置方式将我们项目的用户名和密码配置,然后在通过http方式进行访问。这样也是可以的。
config都会刷新本地文件库的。但是本地文件存储的位置其实是不固定的,项目每次启动当前项目所在的目录都会发生随机改变。文件路径为
config-repo-随机id。会出现这么一种情况当我们重启的时候git挂了这个时候我们将无法获取但是因为随机id的原因我们将获取不到配置信息了。所以
config` 可以让我们指定这个路劲。spring.cloud.config.server.git.basedir: xxxxx
spring.cloud.config.server.git.searchPaths: '{application}'
springcloud config
模块极大的简化了我们微服务中重复配置的问题,默认使用的git来实现公共服务的获取的当然他也是支持svn
,关于svn
的整合呢笔者这里没有指出因为现在使用svn
的公司应该很少了。如果非要使用svn
的话也很简单。将uri
地址换成svn
的就可以了。前提引入如下包<dependency> <groupId>org.tmatesoft.svnkit</groupId> <artifactId>svnkit</artifactId> <version>1.8.10</version></dependency>
nginx是一种高性能HTTP反向代理服务器。在我们之前的项目中我们使用nginx主要有两种用途: 方向代理+静态资源服务器管理
./nginx
或者nginx
。看具体系统http://192.168.44.131:9200
.。我们想要的效果就是通过本地localhost就可以访问到这个服务提供的接口了。server { listen 9200; server_name localhost; location / { proxy_pass http://192.168.44.131:9200; #root html; #index index.html index.htm; } }
nginx -s reload
进行重启nginx才能重新加载配置。这个时候我们访问localhost:9200/zxhtom/start
实际上就是被代理到http://192.168.44.131:9200/zxhtom/start
这个接口上。server{ listen 90; server_name localhost; location / { root D:\study\PageOffice_4.6.0.3_Java; index index.html; } }
D:\study\PageOffice_4.6.0.3_Java
这个文件夹作为本地90端口的入口。我们通过localhost:90/index.html
就能访问到本地的D:\study\PageOffice_4.6.0.3_Java\index.html
文件了。upstream redis { hash $remote_addr consistent; # $binary_remote_addr; server 192.168.44.131:6379 weight=5 max_fails=3 fail_timeout=30s; server 192.168.44.132:6379 weight=1 max_fails=3 fail_timeout=30s; } server { listen 6379;#redis服务器监听端口 proxy_connect_timeout 10s; proxy_timeout 300s;#设置客户端和代理服务之间的超时时间,如果5分钟内没操作将自动断开。 proxy_pass redis; }
upstream
来定义服务,并对服务进行策略定义。最后我们在server中直接时候用upstream
的服务名就可以了。其中的weight就是对集群中服务轮询的权重设置。重启: nginx.exe -s reload
关闭:nginx.exe -s stop
检测配置合法性:nginx.exe -t
Ribbon.Predicate
。想要对灰度发布进行扩展我们就离不开Predicate
//根据输入返回断言 true or false@GwtCompatiblepublic interface Predicate<T> { //针对输入内容进行断言,该方法有且不仅有如下要求: 1、不会造成任何数据污染 2、在T的equals中相等在apply中是相同效果 boolean apply(@Nullable T input); //返回两个Predicate是否相同。一般情况Predicate实现是不需要重写equals的 。 如果实现可以根据自己需求表明predicate是否相同。什么叫做相同就是两个predicate对象apply的结果相同即为对象相同 @Override boolean equals(@Nullable Object object);}
@Test public void pt() { List<User> userList = new ArrayList<>(); for (int i = 0; i < 100; i++) { userList.add(new User(Long.valueOf(i+1), "张三"+(i+1))); } Predicate<User> predicate = new Predicate<User>() { @Override public boolean apply(User user) { return user.getId() % 2 == 0; } }; ArrayList<User> users = Lists.newArrayList(Iterables.filter(userList, predicate)); System.out.println(users); }
AbstractServerPredicate
是Predicate
的实现类。这个类也是Ribbon在获取服务列表的关键角色。因为后面都是基于这个类进行功能扩展的。BaseLoadBalancer
中进行负载均衡的。其内部的rule默认是new RoundRobinRule()
,因为我们引入了io-jmnarloch
。先看看内部的类结构io-jmnarloch
内部不是很复杂,至少Ribbon、feign这些比起来他真的是简单到家了。内部一个四个packagepackage | 作用 |
---|---|
api | 提供上下文,供外部使用 |
predicate | 提供获取服务列表过滤器 |
rule | ribbon中的负载均衡策略实现 |
support | 对上述的辅助包 |
RibbonDiscoveryRuleAutoConfiguration
中配置了rule包下定义好的Ribbon的负载均衡类Rule。BaseLoadBalancer
中我们可以看到rule就是我们rule.MetadataAwareRule
这个类。这里和ribbon章节说的好像有出入,我们在ribbon章节说需要自定义rule的时候需要在@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
这种方式。DiscoveryEnabledRule
的时候在注册的时候有 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
表示作用域MetadataAwareRule
结合代码我们可以了解到最终是PredicateBaseRule#choose 在选择服务列表这个predicate 就是我们choose中getPredicate()方法获取的。所以在ribbon进行选择服务之前会通过MetadataAwarePredicate
进行过滤服务。
获取到过滤器对象后,我们就会执行chooseRoundRibbinAfterFiltering
.
MetadataAwarePredicate
最终继承AbstractServerPredicate
。 而AbstractServerPredicate
# chooseRoundRobinAfterFiltering
是依赖getEligibleServers`来获取合适的服务列表的。AbstractServerPredicate
实现了好多chooseXXX的方法。因为ribbon默认是轮询方式所以在BaseLoadBalance中是选择Round对应的方法。这些我们都可以自己去修改方式。这里不赘述Eligible
译为合适的。getEligibleServers 翻译过来是获取合适的服务列表。这就是我们上述通过metadata-map:lancher
配置我们的服务信息。
下面是AbstractServerPredicate
精简后样子。主要就是getEligibleServers
这个方法。
AbstractServerPredicate
结构图中我们可以看到除了DiscoveryEnabledPredicate
这个子类外,还有四个子类。子类 | 作用 |
---|---|
AvailabilityPredicate | 过滤不可用服务器 |
CompositePredicate | 组合模式,保证服务数量一定数量。换句话说就是服务太少则会一个一个fallback知道服务数量达到要求 |
ZoneAffinityPredicate | 选取指定zone区域内的Server |
ZoneAvoidancePredicate | 避免使用符合条件的server . 和ZoneAffinityPredicate功能相反 |
@RequestMapping(value = "/start",method = RequestMethod.GET) public Map<String,Object> start(@RequestParam Map<String, Object> paramMap) { return testService.startQps(paramMap); }
@Overridepublic Map<String, Object> startQps(Map<String, Object> paramMap) { //根据前端传递的qps上线 Integer times = 100; if (paramMap.containsKey("times")) { times = Integer.valueOf(paramMap.get("times").toString()); } String redisKey = "redisQps"; RedisAtomicInteger redisAtomicInteger = new RedisAtomicInteger(redisKey, redisTemplate.getConnectionFactory()); int no = redisAtomicInteger.getAndIncrement(); //设置时间固定时间窗口长度 1S if (no == 0) { redisAtomicInteger.expire(1, TimeUnit.SECONDS); } //判断是否超限 time=2 表示qps=3 if (no > times) { throw new RuntimeException("qps refuse request"); } //返回成功告知 Map<String, Object> map = new HashMap<>(); map.put("success", "success"); return map;}
我们设置的qps=3 , 我们可以看到五个并发进来后前三个正常访问,后面两个就失败了。稍等一段时间我们在并发访问,前三个又可以正常访问。说明到了下一个时间窗口
滑动时间窗口是将时间更加细化,上面我们是通过redis#setnx实现的。这里我们就无法通过他统一记录了。我们应该加上更小的时间单元存储到一个集合汇总。然后根据集合的总量计算限流。redis的zsett数据结构就和符合我们的需求。
为什么选择zset呢,因为redis的zset中除了值以外还有一个权重。会根据这个权重进行排序。如果我们将我们的时间单元及时间戳作为我们的权重,那么我们获取统计的时候只需要按照一个时间戳范围就可以了。
因为zset内元素是唯一的,所以我们的值采用uuid或者雪花算法一类的id生成器
@RequestMapping(value = "/startList",method = RequestMethod.GET) public Map<String,Object> startList(@RequestParam Map<String, Object> paramMap) { return testService.startList(paramMap); }
String redisKey = "qpsZset"; Integer times = 100; if (paramMap.containsKey("times")) { times = Integer.valueOf(paramMap.get("times").toString()); } long currentTimeMillis = System.currentTimeMillis(); long interMills = inter * 1000L; Long count = redisTemplate.opsForZSet().count(redisKey, currentTimeMillis - interMills, currentTimeMillis); if (count > times) { throw new RuntimeException("qps refuse request"); } redisTemplate.opsForZSet().add(redisKey, UUID.randomUUID().toString(), currentTimeMillis); Map<String, Object> map = new HashMap<>(); map.put("success", "success"); return map;
@RequestMapping(value = "/startLoutong",method = RequestMethod.GET)public Map<String,Object> startLoutong(@RequestParam Map<String, Object> paramMap) { return testService.startLoutong(paramMap);}
@Overridepublic Map<String, Object> startLoutong(Map<String, Object> paramMap) { String redisKey = "qpsList"; Integer times = 100; if (paramMap.containsKey("times")) { times = Integer.valueOf(paramMap.get("times").toString()); } Long size = redisTemplate.opsForList().size(redisKey); if (size >= times) { throw new RuntimeException("qps refuse request"); } Long aLong = redisTemplate.opsForList().rightPush(redisKey, paramMap); if (aLong > times) { //为了防止并发场景。这里添加完成之后也要验证。 即使这样本段代码在高并发也有问题。此处演示作用 redisTemplate.opsForList().trim(redisKey, 0, times-1); throw new RuntimeException("qps refuse request"); } Map<String, Object> map = new HashMap<>(); map.put("success", "success"); return map;}
@Componentpublic class SchedulerTask { @Autowired RedisTemplate redisTemplate; private String redisKey="qpsList"; @Scheduled(cron="*/1 * * * * ?") private void process(){ //一次性消费两个 System.out.println("正在消费。。。。。。"); redisTemplate.opsForList().trim(redisKey, 2, -1); }}
令牌桶和漏桶法是一样的。只不过将桶的作用方向改变了一下。
漏桶的出水速度是恒定的,如果流量突然增加的话我们就只能拒绝入池
但是令牌桶是将令牌放入桶中,我们知道正常情况下令牌就是一串字符当桶满了就拒绝令牌的入池,但是面对高流量的时候正常加上我们的超时时间就留下足够长的时间生产及消费令牌了。这样就尽可能的不会造成请求的拒绝
最后,不论是对于令牌桶拿不到令牌被拒绝,还是漏桶的水满了溢出,都是为了保证大部分流量的正常使用,而牺牲掉了少部分流量
public Map<String, Object> startLingpaitong(Map<String, Object> paramMap) { String redisKey = "lingpaitong"; String token = redisTemplate.opsForList().leftPop(redisKey).toString(); //正常情况需要验证是否合法,防止篡改 if (StringUtils.isEmpty(token)) { throw new RuntimeException("令牌桶拒绝"); } Map<String, Object> map = new HashMap<>(); map.put("success", "success"); return map; }
@Scheduled(cron="*/1 * * * * ?") private void process(){ //一次性生产两个 System.out.println("正在消费。。。。。。"); for (int i = 0; i < 2; i++) { redisTemplate.opsForList().rightPush(redisKey, i); } }
]]>格雷编码是一个二进制数字系统,在该系统中,两个连续的数值仅有一个位数的差异。
给定一个代表编码总位数的非负整数 n,打印其格雷编码序列。即使有多个不同答案,你也只需要返回其中一种。
格雷编码序列必须以 0 开头。
示例 1:
输入: 2
输出: [0,1,3,2]
解释:
00 - 0
01 - 1
11 - 3
10 - 2
对于给定的 n,其格雷编码序列并不唯一。
例如,[0,2,3,1] 也是一个有效的格雷编码序列。
00 - 0
10 - 2
11 - 3
01 - 1
示例 2:
输入: 0
输出: [0]
解释: 我们定义格雷编码序列必须以 0 开头。
给定编码总位数为 n 的格雷编码序列,其长度为 2n。当 n = 0 时,长度为 20 = 1。
因此,当 n = 0 时,其格雷编码序列为 [0]。
0
、1
两个数字。两个数字出现的次数没有限制,位置也没有限制 。然后是在给定的长度限制中对两个数字进行排列组合$$
G{n}^{0}+C{n}^{1}+C{n}^{2}+\cdots+C{n}^{1}
$$
2^n-1
public List<Integer> grayCode(int n) { Double pow = Math.pow(2, n); List<Integer> list = new ArrayList<Integer>(); for (int i = 0; i < pow.intValue(); i++) { list.add(i); } return list;}
01
、10
。 这样的编码就不符合要求。因为两个数字的每一位都不相同。这个就和我们之前的一篇滚雪球的文章一样了。只不过在扩展的地方需要进行镜像反转的操作
还有一点我们需要知道在补零的时候翻译成十进制其实数据大小并没有发生变化
public List<Integer> grayCode(int n) { List<Integer> list = new ArrayList<Integer>(); list.add(0); for (int i = 0; i < n; i++) { for (int j = list.size()-1; j >=0; j--) { list.add((int)Math.pow(2,i) + list.get(j)); } } return list;}
点赞关注吧
]]>编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 “”。
示例 1:
输入:strs = [“flower”,”flow”,”flight”]
输出:”fl”
示例 2:输入:strs = [“dog”,”racecar”,”car”]
输出:””
解释:输入不存在公共前缀。
fl
。public String longestCommonPrefix(String[] strs) { if (strs.length == 0) { return ""; } int minLength = strs[0].length(); String firstStr = strs[0]; for (int i = 1; i < strs.length; i++) { minLength = Math.min(minLength, strs[i].length()); if (minLength == 0) { return ""; } } for (int i = 0; i < minLength; i++) { for (int j = 1; j < strs.length; j++) { if (firstStr.charAt(i) != strs[j].charAt(i)) { return firstStr.substring(0, i); } } } return firstStr.substring(0, minLength);}
$$
f(x) = g(g(g(str[0],str[1]),str[2])….,str[x])
$$
$$
f(x+1)=g(x+1,f(x))
$$
public String longestCommonPrefix2(String[] strs) { if (strs.length == 0) { return ""; } String longest = strs[0]; for (int i = 1; i < strs.length; i++) { if (longest.length() == 0) { return ""; } longest = longestFromTwo(longest, strs[i]); } return longest;}public String longestFromTwo(String a, String b) { int length = Math.min(a.length(), b.length()); for (int i = 0; i < length; i++) { if (a.charAt(i) != b.charAt(i)) { return a.substring(0, i); } } return a.substring(0, length);}
欢迎关注哦
]]>你正在使用一堆木板建造跳水板。有两种类型的木板,其中长度较短的木板长度为shorter,长度较长的木板长度为longer。你必须正好使用k块木板。编写一个方法,生成跳水板所有可能的长度
返回的长度需要从小到大排列。
示例 1
输入:
shorter = 1
longer = 2
k = 3
输出: [3,4,5,6]
解释:
可以使用 3 次 shorter,得到结果 3;使用 2 次 shorter 和 1 次 longer,得到结果 4 。以此类推,得到最终结果。
递归
、记忆化
。$$
result[0] = shorter * k ;
$$
K+1
。而第一个元素是shorter*k。$$
f(i) = shorter(k-i) + longer i
$$
$$
f(i+1)-f(i) = (shorter (k-(i+1)) + longer (i+1)) - (shorter(k-i) + longer i) = shorter (-1) + longer 1 = longer - shorter
$$
$$
f(i+1) = (longer-shorter) + f(i)
$$
public int[] divingBoard(int shorter, int longer, int k) { if (k == 0) { return new int[]{}; } if (shorter == longer) { return new int[]{shorter * k}; } int[] arr = new int[k+1]; arr[0] = k * shorter; for (int i = 1; i < arr.length; i++) { arr[i] = (longer - shorter) + arr[i - 1]; } return arr;}
点赞呐
]]>给定一个非负整数数组
nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
0~2
这三块节点都是可达的。如果我们想通过起点不断的跳跃可以到达终点位置,那么就得在0~2这三个节点中不断将可达区间扩散。扩散的依据就是每个节点的可跳跃范围。从图中我们可知当在i=1时我们可以跳跃范围是3,那么就是1~4块节点都是可以跳跃达到的。public boolean canJump(int[] nums) { int max = 0; for (int i = 0; i <= max; i++) { max = Math.max(max, i + nums[i]); if (max+1 >= nums.length) { return true; } } return false;}
点赞环节哦
]]>给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
public ListNode addTwoNumbers(ListNode l1, ListNode l2) { List<Integer> list1 = getTotalFromListNode(l1); List<Integer> list2 = getTotalFromListNode(l2); List<Integer> totalList = addListInteger(list1,list2); ListNode node = getListNodeFromTotal(totalList); return node;}
/** * 两个list相加 , 模拟加法 * @param list1 * @param list2 * @return */private List<Integer> addListInteger(List<Integer> list1, List<Integer> list2) { List<Integer> list = new ArrayList<>(); Integer length = list1.size() > list2.size() ? list1.size() : list2.size(); //进位管理器 Integer up = 0; Integer start = 0; Integer end = 0; for (Integer i = 0; i < length; i++) { if (i < list1.size()) { start = list1.get(i); } if (i < list2.size()) { end = list2.get(i); } int total = start + end + up; //total个位存入新的list , 十位存入进位管理器 up = total / 10; list.add(total % 10); //清空 start = 0; end = 0; } //最后判断进位管理器是否有进位数据 if (up != 0) { list.add(up); } return list;}private ListNode getListNodeFromTotal(List<Integer> totalList) { ListNode node = new ListNode(0); ListNode temNode = node; for (int i = 0; i < totalList.size(); i++) { ListNode subNode = new ListNode(totalList.get(i)); temNode.next = subNode; temNode = subNode; } return node.next;}private List<Integer> getTotalFromListNode(ListNode node) { List<Integer> list = new ArrayList<>(); while (node != null) { list.add(node.val); node = node.next; } return list;}
点赞哈
]]>给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。你可以假设 nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]示例 2:
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
public void merge(int[] nums1, int m, int[] nums2, int n) { int p1 = m - 1; int p2 = n - 1; for (int i = nums1.length - 1; i >= 0; i--) { int num1=Integer.MIN_VALUE, num2 = Integer.MIN_VALUE; if (p1 >= 0) { num1 = nums1[p1]; } if (p2 >= 0) { num2 = nums2[p2]; } if (num1 > num2) { p1--; nums1[i] = num1; } else { p2--; nums1[i] = num2; } }}
点赞+评论哦
]]>给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
public int[] twoSum(int[] nums, int target) { for (int i = 0; i < nums.length; i++) { for (int j = i+1; j < nums.length; j++) { if (target == nums[i] + nums[j]) { return new int[]{i, j}; } } } return new int[2];}
public int[] twoSum(int[] nums, int target) { int[] arr = new int[2]; int index = 0; Map<Integer, Integer> map = new HashMap<>(); for (int i = 0; i < nums.length; i++) { map.put(nums[i], i); } for (int i = 0; i < nums.length; i++) { int sub = target - nums[i]; if (map.containsKey(sub) && i != map.get(sub)) { arr[0] = i; arr[1] = map.get(sub); break; } } return arr;}
public int[] twoSum3(int[] nums, int target) { int[] arr = new int[2]; int index = 0; Map<Integer, Integer> map = new HashMap<>(); for (int i = 0; i < nums.length; i++) { int sub = target - nums[i]; if (map.containsKey(sub) && i != map.get(sub)) { arr[0] = i; arr[1] = map.get(sub); break; } map.put(nums[i], i); } return arr;}
点赞、关注、收藏吧
]]>