Algorithm
每周至少做一个Leetcode算法题
第1道
【来源】
《剑指Offer》12#
【题目】
设计一个函数,输入整数n,打印1到最大的n位数
【例子】
输入:3
输出:1,2,...,998,999
解释:最大的3位数是999
【解答】
题目未指定n的大小,需考虑到大数问题,常用的处理大数问题的数据结构是字符串或者数组,本题用字符串来处理。
解法1:字符串模拟数字加法。
掌握2个关键步骤问题迎刃而解,1)模拟加1操作,注意处理溢出;2)打印
解法2:递归实现全排列
本题等价于n个数字0~9的全排列,用递归很容易实现。基于分治算法的思想,先固定高位,向低位递归,当个位已被固定时,打印字符串。
【示例代码】
package com.pengluo.hht_offer.T12_PrintNumbers;
public class Print1ToMaxOfN {
/**
* 解法1:字符串模拟数字加法
* @param n
*/
public void print1ToMaxOfNBit(int n) {
if (n <= 0) {
return;
}
// 定义并初始化数组,每个字符的初始值均为'0'
// 约定:number[0]:最高位,number[n-1]:最低位
char[] number = new char[n];
for (int i = 0; i < n; i++) {
number[i] = '0';
}
// 发生溢出时,结束打印
while (!increment(number)) {
printNumber1(number);
}
}
/**
* 辅助方法:模拟字符串数字加1
* @param number
* @return true表示发生溢出,flase表示正常
*/
private boolean increment(char[] number) {
// 溢出标志位
boolean isOverFlow = false;
// 进位
int TakeOver = 0;
// 每个字符表示的数字
int Snum =0;
int length = number.length;
for (int i = length-1; i >= 0; i--) {
Snum = number[i] - '0' + TakeOver;
if (i == length-1) {
Snum++;
}
// 处理进位逻辑
if (Snum >= 10) {
if (i == 0) {
isOverFlow = true;
}
TakeOver = 1;
Snum -= 10;
number[i] = (char) ('0' + Snum);
} else {
number[i] = (char) ('0' + Snum);
break;
}
}
return isOverFlow;
}
/**
* 辅助方法:打印
* @param number
*/
private void printNumber1(char[] number) {
boolean flag = false;
int length = number.length;
for (int i = 0; i <length; i++) {
// 数字左边的0不输出
if (number[i] != '0') {
flag = true;
}
if (flag) {
System.out.print(number[i]);
}
}
System.out.println();
}
/**
* 解法2:递归法实现全排列
*
* @param n
*/
public void print1ToMaxNBitByRecursively(int n) {
if (n <= 0) {
return;
}
char[] number =new char[n];
for (int i = 0; i < 10; i++) {
number[0] = (char) (i + '0');
printByRecursively(number, n, 0);
}
}
private void printByRecursively(char[] number, int length, int index) {
// 递归结束条件
if (index == length - 1) {
printNumber1(number);
return;
}
for (int i = 0; i < 10; i++) {
number[index + 1] = (char) (i + '0');
printByRecursively(number, length, index+1);
}
}
/**
* 错误范例:未统一number表示的数字格式,此方法,默认number[0]为最低位
* @param number
*/
private void printNumber(char[] number) {
boolean flag = false;
int length = number.length;
for (int i = length - 1; i >= 0; i--) {
// 数字左边的0不输出
if (number[i] != '0') {
flag = true;
}
if (flag) {
System.out.print(number[i]);
}
}
System.out.println();
}
}
【拓展】
-
将本题的打印改成返回一个int[],数组存储所有的打印结果
-
设计一个函数,可以输出两个数相加的结果。注意输入的参数可能是负数
【总结】
-
Int、long 等类型都有表示的数字范围,不能处理大数。需要用String表示大数
-
解法1是用字符串模拟数字加法运算
-
解法2是用分治思想,递归实现
Review
阅读并点评至少1篇英文技术文章
【原文】:Head First Java 2nd Edition CH16&core java CH8
【点评】
泛型机制是从Java 5开始支持的,专家组花了将近5年时间定义和规范。泛型在集合中应用广泛,ArrayList就是应用最多的集合之一。
泛型编程的优点是可读性好、安全性高、代码可重用
类型参数(type parameters)
// 添加类型参数:<String>
// java 7开始,构造函数中的泛型类型可以省略
ArrayList<String> files = new ArrayList<>();
做一个泛型程序员
泛型程序员的任务是:预测所用类未来所有可能有的用途。这相当难却很有价值。
大多数Java程序员满足于API调用
定义一个简单的泛型类(Generic class)
// 类型变量:<T>,它指定了方法的返回类型,域、局部变量的类型都为T
public class Pair<T> {
}
定义一个简单的泛型方法
public class ArrayAlg{
public static <T> T getMiddle(T[] a) {
return a[a.length/2];
}
}
类型变量的限定
public class ArrayAlg{
// 在泛型中,`extends`是`extends` 或`implements`的意思
// 限定一个
// 表示只有实现Comparable接口的类可以传入,否则编译报错
public static <T extends Comparable> T min(T[] a) {
}
// 限定类型用&连接
public static <T extends Comparable & Serializle> T min(T[] a) {
}
}
泛型代码和虚拟机
JVM里没有泛型代码,全部是普通的类。JVM自动通过类型擦除把泛型类变成原始类型
类型擦除
- 用第一个限制类代替泛型变量T,没有明确限制则默认用Object代替T
// 擦除类型:去掉class上的<T>,域和成员变量T换成对应的类
// 1) public class Pair<T> {}
T-->Object
// 2)public class <T extends Comparable & Serializle> implements Serializle {}
T-->Comparable
// 3)public class <T extends Serializle & Comparable> implements Serializle {}
T-->Serializle
JVM自动生成桥方法,用来处理多态
通配符类型
如果是一名库程序员,一定要习惯通配符类型
// 使用通配符,也就是说Employee的所有子类都可以做方法的参数.可以读取(getter)
public static void printBuddies(Pair<? extends Employee> p)
{
Employee first = p.getFirst();
Employee second = p.getSecond();
System.out.println(first.getName() + " and " + second.getName() + " are buddies.");
}
// 超类限制,Manager及其子类可以做方法的参数,可以写入(setter)
public static void minmaxBonus(Manager[] a, Pair<? super Manager> result)
{
if (a.length == 0) return;
Manager min = a[0];
Manager max = a[0];
for (int i = 1; i < a.length; i++)
{
if (min.getBonus() > a[i].getBonus()) min = a[i];
if (max.getBonus() < a[i].getBonus()) max = a[i];
}
result.setFirst(min);
result.setSecond(max);
}
// 无限定通配符
class PairAlg
{
public static boolean hasNulls(Pair<?> p)
{
return p.getFirst() == null || p.getSecond() == null;
}
public static void swap(Pair<?> p) { swapHelper(p); }
public static <T> void swapHelper(Pair<T> p)
{
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
}
Tip
学习至少一个技术技巧
缓存穿透、缓存击穿、缓存雪崩的问题由来,解决方法
缓存穿透:缓存和数据库都没有命中,大部分场景时恶意攻击,大量的请求到达数据库,数据库不堪重负崩了
解决方案:
- 接口层做校验,用户鉴权校验
- 布隆过滤器:将所有存在的数据哈希存到位图中,那么布隆过滤器判断不存在,那么数据库中一定没有,拦截即可。相比直接存到缓存redis中,它的优点是节省空间,比如10亿条数据存到Map等数据结构中,占用900G的空间。大概了解
- id做基础校验,id<0(非法参数)直接拦截
- 数据库未命中时,设置短时间的空值缓存,如1分钟。缓解数据库压力
缓存击穿:缓存中没有,数据库中有。通常是:缓存时间到期,此时有大量并发请求,缓存中取不到数据,数据库瞬间压力很大。
解决方案:
- 热点数据永不过期
- 查询DB过程加互斥锁
- Redis的SETNX
- Memcache的ADD
- ReentrantLock
缓存雪崩:缓存中数据批量的到期,需要去数据库查询,数据库压力大增。和缓存击穿不同点在,后者是同一key大量并发请求,
前者这里是很多key的缓存都集体失效
解决方案:
- 缓存过期时间设置为随机,避免一个时间同时过期
- 用分布式,将热点数据均匀存在不同redis数据节点
- 热点数据永不过期
Share
分享一篇有观点和思考的技术文章