MacOS软件推荐

MACOS软件推荐

系统设置

  • 时钟屏保:fliqo
  • 屏幕触发角,位于屏幕保护选项处,设置右上为通知中心,右下为启动器
  • dock栏大小调整,位于程序坞

系统工具

  • 搜狗输入法

  • Clean MyMac

  • chrome浏览器

  • NTFS破解版

  • 解压缩:Dr unachriver

  • 截图: snip

  • 快捷键提示: cheatsheet

  • Qbserve 追踪用户使用mac软件消耗的时间,工作时间记录

  • hellofont字体下载

工作效率

  • Wunderlist 奇妙清单

  • 日历工具 - Fantastical 2 for Mac

  • 日历工具 pendo
  • xmind pro 脑图
  • Alfred 快捷键、工作流,替代聚焦搜索
  • self control
  • office 3件套
  • Snipaste 截图 + 贴图
  • Paste 剪切板管理
  • OmniGraffle:专业制图工具

阅读写作

  • typaro

  • Tickeys 打字有触发声音

  • 欧陆词典

  • PDF expert

  • 专业杂志 英文PDF 教材阅读 :MarginNote pro

开发

  • homebrew

  • 基础环境 python java go git

  • Anaconda

  • IDEA全家桶 IDEA、Pycharm、Goland

  • VS code,替代sublime text

  • oh-my-zsh + iterm2,选择合适的终端配色方案,然后窗口调整成半透明的

  • Dash 文档阅读速查

  • parallel desktop 虚拟机

  • docker

  • Charles和Wireshark http,流量抓包

影音娱乐

  • IINA ,github,homebrew安装
  • 网易云音乐

游戏

  • steam

下载

  • 百度网盘
  • 迅雷
  • Aria2 + Aria2GUI + chrome插件

聊天

  • QQ
  • 微信

Go学习笔记

Go 学习笔记

hello world

环境配置

  • 安装go

  • 设置环境变量

    • GOROOT 安装go的根目录
    • path: 添加 $GOROOT/bin/
    • GOPATH: go project的目录
  • 安装调试工具

    go get http://github.com/derekparker/delve/cmd/dlv

    如果安装失败,需要删掉derekparker的目录,然后重新执行

    idea的go插件无法debug,使用go目录里的dlv替换idea插件目录里的。用everything快速定位文件所在目录。

go特性

  1. 垃圾回收
    a. 内存自动回收,再也不需要开发人员管理内存
    b. 开发人员专注业务实现,降低了心智负担
    c. 只需要new分配内存,不需要释放

  2. 天然并发
    a. 从语言层面支持并发,非常简单
    b. goroute,轻量级线程,创建成千上万个goroute成为可能
    c. 基于CSP(Communicating Sequential Process)模型实现

    1
    2
    3
    func main() {
    go fmt.Println(“hello")
    }
  3. channel
    a. 管道,类似unix/linux中的pipe
    b. 多个goroute之间通过channel进行通信
    c. 支持任何类型

    1
    2
    3
    4
    5
    func main() {
    pipe := make(chan int,3)
    pipe <- 1
    pipe <- 2
    }
  4. 多返回值
    a. 一个函数返回多个值

    1
    2
    3
    4
    5
    func calc(a int, b int)(int,int) {
    sum := a + b
    avg := (a+b)/2
    return sum, avg
    }

helllo go

1
2
3
4
5
6
7
8
9
package main
import(
“fmt”
)
func main() {
fmt.Println(“hello world”)
}

  1. 和python一样,把相同功能的代码放到一个目录,称之为包
  2. 包可以被其他包引用,使用import
  3. main包是用来生成可执行文件,每个程序只有一个main包
  4. 包的主要用途是提高代码的可复用性
  5. 访问可见性,包里的函数必须首字母大写才能被其他包访问
  6. go get安装第三方包

基本数据类型和操作符

文件名&关键字&标识符

  1. 所有go源码以.go结尾

  2. 标识符以字母或下划线开头,大小写敏感,比如:

  3. _是特殊标识符,用来忽略结果

  4. 保留关键字

    | break | default | func | interface | select |
    | ——– | ———- | —— | ——— | —— |
    | case | defer | go | map | struct |
    | chan | else | goto | package | switch |
    | const | fallthough | if | range | type |
    | continue | for | import | return | var |

go程序的基本结构

  1. 任何一个代码文件隶属于一个包
  2. import 关键字,引用其他包。包的相对路径是GOPATH下的src文件夹
  3. import包时会执行init函数
  4. golang可执行程序, package main,并且有且只有一个main入口函数
  5. 包中函数调用:
    a. 同一个包中函数,直接调用
    b. 不同包中函数,通过包名+点+函数名进行调用
  6. 包访问控制规则:
    a. 大写意味着这个函数/变量是可导出
    b. 小写意味着这个函数/变量是私有的,包外部不能访问
1
2
3
4
5
import (
"fmt"
a "test/utils" // 包的别名
_ "xxx" // 只执行包里的init
)

函数声明和注释

  1. 函数声明:func 函数名字 (参数列表) (返回值列表) {}
  2. 注释,两种注释,单行注释: // 和多行注释 / /

常量

  1. 常量使用const 修饰,代表永远是只读的,不能修改。

  2. const 只能修饰boolean, number(int相关类型、浮点类型、 complex)和string。

  3. 语法: const identifier [type] = value,其中type可以省略。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const b string = “hello world”
    const b = “hello world”
    const Pi = 3.1414926
    const a = 9/3
    const c = getValue()
    // 枚举
    const (
    a = 0
    b = 1
    c = 2
    )
    const (
    a = iota
    b //1
    c //2
    )

    变量

    1
    2
    3
    4
    5
    6
    7
    Var (
    a int //默认为0
    b string //默认为””
    c bool //默认为false
    d int = 8
    e string = “hello world”
    )

值类型和引用类型

  1. 值类型:变量直接存储值,内存通常在栈中分配。var i = 5

    基本数据类型int、 float、 bool、 string以及数组和struct。

  2. 引用类型:变量存储的是一个地址,这个地址存储最终的值。内存通常在
    堆上分配。通过GC回收。

    引用类型:指针、 slice、 map、 chan等都是引用类型。

变量的作用域

  1. 在函数内部声明的变量叫做局部变量,生命周期仅限于函数内部。
  2. 在函数外部声明的变量叫做全局变量,生命周期作用于整个包,如果是大写的,
    则作用于整个程序。

数据类型和操作符

  1. bool类型,只能存true和false,默认值为false

    相关操作符,!、 &&、 ||

  2. 数字类型,主要有int、 int8、 int16、 int32、 int64、 uint8、 uint16、 uint32、 uint64,无符号整型

  3. 类型转换,type(variable), 比如: var a int=8; var b int32=int32(a)

  4. 逻辑操作符: == 、 !=、 <、 <=、 >和 >=

  5. 数学操作符: +、 -、 *、 /、%等等

  6. 字符类型: byte,0~127对应的ASCII码

  7. 字符串类型:

    字符串表示两种方式: 双引号 和反引号,反引号为raw字符串。

    两个字符串拼接 +,字符串底层是byte数组,支持index和切片

strings和strconv使用

  1. strings.HasPrefix(s string, prefix string) bool:判断字符串s是否以prefix开
  2. strings.HasSuffix(s string, suffix string) bool:判断字符串s是否以suffix结

  3. strings.Index(s string, str string) in:判断str在s中首次出现的位置,如果出现,则返回-1

  4. strings.LastIndex(s string, str string) int:判断str在s中最后出现的位置,出现,则返回-1

  5. strings.Replace(str string, old string, new string, n int):字符串替换

  6. strings.Count(str string, substr string)int:字符串计数
  7. strings.Repeat(str string, count int)string:重复count次str
  8. strings.ToLower(str string)string:转为小写
  9. strings.ToUpper(str string)string:转为大写
  10. strings.TrimSpace(str string):去掉字符串首尾空白字符
  11. strings.Trim(str string, cut string):去掉字符串首尾cut字符
  12. strings.TrimLeft(str string, cut string):去掉字符串首cut字符
  13. strings.TrimRight(str string, cut string):去掉字符串首cut字符
  14. strings.Field(str string):返回str空格分隔的所有子串的slice
  15. strings.Split(str string, split string):返回str split分隔的所有子串的s
  16. strings.Join(s1 []string, sep string):用sep把s1中的所有元素链接起来
  17. strings.Itoa(i int):把一个整数i转成字符串
  18. strings.Atoi(str string)(int, error):把一个字符串转成整数

时间和日期类型

time.Time类型,用来表示时间

  1. 获取当前时间, now := time.Now()
  2. time.Now().Day(), time.Now().Minute(), time.Now().Month(), time.Now().Ye
  3. 格式化, fmt.Printf(“%02d/%02d%02d %02d:%02d:%02d”, now.Year()…)

    time.Duration用来表示纳秒

1
2
3
4
5
6
7
8
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)

格式化:

1
2
3
4
5
// layout 必须是 2006-01-02 15:04:05.999999999,不能用其他的数字, 15点也可以是3点
now := time.Now()
fmt.Println(now.Format(“02/1/2006 15:04”))
fmt.Println(now.Format(“2006/1/02 15:04”))
fmt.Println(now.Format(“2006/1/02”))

指针类型

  1. 普通类型,变量存的就是值,也叫值类型
  2. 获取变量的地址,用&,比如: var a int, 获取a的地址: &a
  3. 指针类型,指针变量存的是一个地址,这个地址存的才是值。给指针变量赋值必须是一个地址。
  4. 获取指针类型所指向的值,使用: *,比如: var *p int, 使用*p获取p指向的值

流程控制

  1. If / else分支判断

  2. switch case语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    switch varX {
    case var1:
    fallthrough // 默认自带break
    case var2:
    case var3, var4, var5: //多个值
    default:
    }
    switch{
    case condition1:
    case condition2:
    }
  3. for 语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    for i := 0 ; i < 100; i++ {
    break
    continue
    }
    for i > 0 {
    fmt.Println(“i > 0”)
    }
    for true {} // 等价于 for{}
    for i, v := range str {
    }
    // range 用来遍历切片、map、chan、str等
  4. goto 和 label 语句

函数

  1. 声明语法: func 函数名 (参数列表) [(返回值列表)] {}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    func add() {
    }
    func add(a int, b int) {
    }
    func add(a int, b int) int {
    }
    func add(a int, b int) (low int, max int) {
    }
    func add(a, b int) (int, int) {
    }
  2. golang函数特点:
    a. 不支持重载,一个包不能有两个名字一样的函数
    b. 函数是一等公民,函数也是一种类型,一个函数可以赋值给变量
    c. 匿名函数
    d. 多返回值

  3. 函数参数传递方式:
    1). 值传递
    2). 引用传递

    注意1:无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。

    注意2: map、 slice、 chan、指针、 interface默认以引用的方式传递

  4. 命名返回值的名字:func add(a, b int) (c int){}

    _标识符,可以接收想要忽略的函数返回值

  5. 可变参数:
    func add(arg…int) int {} 0个或多个参数

    func add(a int, b int, arg…int) int {} 2个或多个参数
    注意:其中arg是一个slice,我们可以通过arg[index]依次访问所有参数;通过len(arg)来判断传递参数的个数

  6. defer用途:

    1. 当函数返回时,执行defer语句。因此,可以用来做资源清理
    2. 多个defer语句,按先进后出的方式执行
    3. defer语句中的变量,在defer声明时就决定了。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 关闭文件句柄
    file := open(filename)
    defer file.Close()
    // 锁资源释放
    mc.Lock()
    defer mc.Unlock()
    // 数据库、网络连接
    conn := openDatabase()
    defer conn.Close()

内置函数

  1. close:主要用来关闭channel
  2. len:用来求长度,比如string、 array、 slice、 map、 channel
  3. new:用来分配内存,主要用来分配值类型,比如int、 struct。返回的是指针
  4. make:用来分配内存,主要用来分配引用类型,比如chan、 map、 slice
  5. append:用来追加元素到数组、 slice中
  6. panic和recover:用来做错误处理
  7. new和make的区别
  8. 一个函数调用自己,就叫做递归。
    递归的设计原则
    递归函数
    1)一个大的问题能够分解成相似的小问题
    2)定义好出口条件

闭包

  1. 闭包:定义的一个函数和与其相关的引用环境组合而成的实体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package main
    import (
    "fmt"
    "strings"
    )
    func makeSuffixFunc(suffix string) func(string) string {
    // 返回一个函数
    var x string // 如果函数引用了x这个变量,x在其生命周期一直存在
    return func(name string) string {
    if !strings.HasSuffix(name, suffix) {
    return name + suffix
    }
    return name
    }
    }
    func main() {
    func1 := makeSuffixFunc(".bmp")
    func2 := makeSuffixFunc(".jpg")
    fmt.Println(func1("test"))
    fmt.Println(func2("test"))
    }

数组和切片

  1. 数组:是同一种数据类型的固定长度的序列。

    数组定义: var a [len]int, len必须是整数常量,不能传入变量,a [10]int

    长度是数组类型的一部分,因此, var a[5] intvar a[10]int是不同的类型

  2. 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是: len-1

  3. 访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic

  4. 数组是值类型,因此改变副本的值,不会改变本身的值
    arr2 := arr1 arr2[2] = 100 arr1的值不会变

  5. 数组初始化

    1
    2
    3
    4
    5
    var age1 = [5]int{1,2,3,4,5}
    var age2 = […]int{1,2,3,4,5,6}
    var age0 [5]int = [5]int{1,2,3}
    var str = [5]string{3:”hello world”, 4:”tom”}
    var a [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}

切片

  1. 切片:切片是数组的一个引用,因此切片是引用类型

  2. 切片的长度可以改变,因此,切片是一个可变的数组

  3. 切片遍历方式和数组一样,可以用len()求长度,cap可以求出slice最大的容量, 0 <= len(slice) <= (array), 其中array是slice引用的数组

  4. 切片的定义:var 变量名 []类型,比如var str []string var arr []int

  5. 切片初始化:

    1
    2
    3
    4
    var slice []int = arr[start:end]
    var slice []int=arr[:end]
    var slice []int = arr[start]
    var slice []int = arr[:]
  6. 通过make来创建切片

    1
    2
    3
    var slice []type = make([]type, len)
    slice := make([]type, len)
    slice := make([]type, len, cap)
  7. 用append内置函数,在切片尾部添加元素

  8. For range 遍历切片 for index, val := range slice {}

  9. 切片resize

    1
    2
    3
    var a = []int {1,3,4,5}
    b := a[1:2]
    b = b[0:3]
  10. 切片拷贝

    1
    2
    3
    4
    5
    6
    s1 := []int{1,2,3,4,5}
    s2 := make([]int, 10)
    copy(s2, s1)
    s3 := []int{1,2,3}
    s3 = append(s3, s2…)
    s3 = append(s3, 4,5,6)
  11. string与slice,string底层就是一个byte的数组,因此,也可以进行切片操作

  12. 如何改变string中的字符值?
    string本身是不可变的,因此要改变string中字符,只能建立新的字符串

  13. 排序和查找操作

    1
    2
    3
    4
    5
    6
    sort.Ints()//对整数进行排序,
    sort.Strings() // 对字符串进行排序,
    sort.Float64s() //对浮点数进行排序.
    sort.SearchInts(a []int, b int) //从数组a中查找b,前提是a必须有序
    sort.SearchFloats(a []float64, b float64) //从数组a中查找b,前提是a必须有序
    sort.SearchStrings(a []string, b string) //从数组a中查找b,前提是a必须有序

map

key-value的数据结构,又叫字典或关联数组

  • 声明

    1
    2
    3
    4
    5
    var map1 map[keytype]valuetype
    var a map[string]string
    var a map[string]int
    var a map[int]string
    var a map[string]map[string]string = make( map[string]map[string]string,16)
  1. map相关操作
    插入和更新、查找、遍历
    delete(a, “hello”)删除
    len(a) 长度
  2. map是引用类型
  3. map排序
    a. 先获取所有key,把key进行排序
    b. 按照排序好的key,进行遍历

线程同步

  • import(“sync”)
  • 互斥锁, var mu sync.Mutex
  • 读写锁, var mu sync.RWMutex
  • go build -race xxx.go race参数可以帮助分析是否有数据竞争

结构体和方法

struct

  1. 用来自定义复杂数据结构,Go语言没有class类型,只有struct类型
  2. struct里面可以包含多个字段(属性), 在内存上是连续分配的
  3. struct类型可以绑定方法
  4. struct类型是值类型,使用new初始化
  5. struct类型可以嵌套
  6. 我们可以为struct中的每个字段,写上一个tag。这个tag可以通过反射的机制获取到,最常用的场景就是json序列化和反序列化
1
2
3
4
5
6
7
8
9
10
type Student struct {
Name string "tag name"
Age int `json:"age"`
Score int
}
var stu Student
stu.Name = “tony”
stu.Age = 18
stu.Score=20
fmt.Printf(“name=%s age=%d score=%d”, stu.Name, stu.Age, stu.Score)

用struct构建链表和树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Student struct {
Name string
Next* Student
}
type Student struct {
Name string
Next* Student
Prev* Student
}
type Student struct {
Name string
left* Student
right* Student
}

工厂模式创建struct

golang中的struct没有构造函数,一般可以使用工厂模式来解决这个问题

1
2
3
4
5
6
7
8
9
10
11
12
type student struct {
Name stirng
Age int
}
func NewStudent(name string, age int) *student {
return &student{
Name:name,
Age:age,
}
}
S := model.NewStudent(“tony”, 20)

匿名字段

1
2
3
4
5
6
7
8
9
10
11
12
13
type Car struct {
Name stirng
Age int
}
type Train struct {
Car
int
Name // 冲突字段
Start time.Time
}
train.int
train.Name
train.Car.Name = "train"

方法

  1. Golang中的方法是作用在特定类型的变量上,因此自定义类型都可以有方法,而不仅仅是struct
    方法定义: func (recevier type) methodName(参数列表)(返回值列表){}

    1
    2
    3
    4
    5
    func (this A) test() {
    fmt.Println(this.a)
    }
    var t A
    t.test()
  2. 方法的调用receiver.method(args)

  3. 指针receiver vs 值receiver,本质上和函数的值传递和地址传递是一样的

  4. 方法的访问控制,通过大小写控制

  5. 方法继承: struct嵌套的匿名结构体,匿名结构体的方法可以直接调用,从而实现了继承。

  6. 组合和匿名字段
    方法
    如果一个struct嵌套了另一个匿名结构体,那么这个结构可以直接访问
    匿名结构体的方法,从而实现了继承。
    如果一个struct嵌套了另一个有名结构体,那么这个模式就叫组合。

  7. 多重继承 一个struct嵌套多个匿名结构体,那么这个结构可以继承多个匿名结构体的方法

  8. 实现String():方法如果一个变量实现了String()这个接口的方法,那么fmt.Println就会调用该方法。

接口

interface

Interface类型可以定义一组方法,但是这些不需要实现。并且interface不能包含任何变量。

interface类型默认是一个指针.

1
2
3
4
type example interface{
Method1(参数列表) 返回值列表
Method2(参数列表) 返回值列表
}
  1. 接口实现
    a. Golang中的接口,不需要显示的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此, golang中没有implement类似的关键字
    b. 如果一个变量含有了多个interface类型的方法,那么这个变量就实现了多个接口。
    c. 如果一个变量只含有了1个interface的部分方法,那么这个变量没有实现这个接口。

  2. 多态: 一种事物的多种形态,都可以按照统一的接口进行操作

  3. 接口嵌套 :一个接口可以嵌套在另外的接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    type ReadWrite interface {
    Read(b Buffer) bool
    Write(b Buffer) bool
    }
    type Lock interface {
    Lock()
    Unlock()
    }
    type File interface {
    ReadWrite
    Lock
    Close()
    }
  4. 类型断言,由于接口是一般类型,如果要转成具体类型,需要使用类型断言。或者采用type switch方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var t int
    var x interface{}
    x = t
    y = x.(int) //转成int
    y, ok = x.(int) //转成int,带检查
    switch x.(type) {
    case *Circle: fmt.Printf(“Type Circle %T with value %v\n”, t, t)
    case bool: fmt.Printf(“param #%d is a bool\n”
    case float64: fmt.Printf(“param #%d is a float64\
    case int, int64: fmt.Printf(“param #%d is an int\n”
    case nil: fmt.Printf(“param #%d is nil\n”, i)
    case string: fmt.Printf(“param #%d is a string\n”,
    default: fmt.Printf(“param #%d’s type is unknown\n
    }
  5. 空接口,Interface{}空接口没有任何方法,所以所有类型都实现了空接口。

  6. 判断一个变量是否实现了指定接口, 同样使用类型断言。

    1
    2
    3
    4
    5
    6
    7
    type Stringer interface {
    String() string
    }
    var v MyStruct
    if sv, ok := v.(Stringer); ok {
    fmt.Printf(“v implements String(): %s\n”, sv.String());
    }

sort.Interface

被排序的对象必须实现Interface这个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less reports whether the element with
// index i should sort before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}
func Sort(data Interface) {
n := data.Len()
quickSort(data, 0, n, maxDepth(n))
}

反射

反射:可以在运行时动态获取变量的相关信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import (“reflect”)
func test_reflact(b interface{}) {
fmt.Println("type: ", reflect.TypeOf(b)) //获取变量的类型
fmt.Println("value: ", reflect.ValueOf(b))
v := reflect.ValueOf(b) //获取变量的值,返回reflect.Value类型
fmt.Println("kind: ", v.Kind())//获取变量的类别,返回一个常量
fmt.Println("type: ", v.Type())
fmt.Println("value: ", v.Float()) // 返回Float
iv := v.Interface() //reflect.Value.Interface() 转换成interface{}类型变量
x, ok := iv.(float64)
if ok {
fmt.Println(x)
}
}
  1. reflect.Value.Kind()方法返回的常量

    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
    const (
    Invalid Kind = iota
    Bool
    Int
    Int8
    Int16
    Int32
    Int64
    Uint
    Uint8
    Uint16
    Uint32
    Uint64
    Uintptr
    Float32
    Float64
    Complex64
    Complex128
    Array
    Chan
    Func
    Interface
    Map
    Ptr
    Slice
    String
    Struct
    UnsafePointer
    )
  2. 获取变量的值:

    1
    2
    3
    4
    reflect.ValueOf(x).Float()
    reflect.ValueOf(x).Int()
    reflect.ValueOf(x).String()
    reflect.ValueOf(x).Bool()
  3. 通过反射的来改变变量的值

    1
    2
    3
    4
    5
    6
    7
    8
    // reflect.Value.SetXX相关方法,比如:
    reflect.Value.SetFloat()//设置浮点数
    reflect.Value.SetInt() //设置整数
    reflect.Value.SetString() //设置字符串
    a := 3.14
    fv := reflect.ValueOf(&a)
    fv.Elem().SetFloat(3.3)
    fmt.Printf("%v\n", a)

    修改变量时要传入地址,访问变量时无法用*, 可以用 p.Elem()

  4. 用反射操作结构体

    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
    type Rf_obj struct {
    Name string `json:"name"`
    }
    func (rf *Rf_obj) Print(){}
    func (rf *Rf_obj) Set(){}
    func test(b interface{}) {
    // 获取struct tag
    tp := reflect.TypeOf(b)
    fmt.Println("type field tag: ", tp.Elem().Field(0).Tag.Get("json"))
    vP := reflect.ValueOf(b)
    if vP.Kind() == reflect.Ptr {
    fmt.Println("这是个指针kind")
    }
    vV := vP.Elem()
    if vV.Kind() == reflect.Struct {
    fmt.Println("通过指针获取到了struct")
    }
    // 类型的Value上有field和部分方法
    fmt.Println("struct field num: ", vV.NumField()) // 获取结构体中字段的个数
    fmt.Println("field: ", vV.Field(0))
    vV.Field(0).SetString("yyyy")
    // 指针type上绑定的方法
    fmt.Println("*ptr's method num: ", vP.NumMethod()) // 获取方法的个数
    vP.Method(0).Call(nil) // 来调用结构体中的方法
    vP.Method(1).Call(nil)
    vP.Method(0).Call(nil)
    }

IO和Json序列化

标准IO

  • os.Stdin:标准输入
  • os.Stdout:标准输出
  • os.Stderr:标准错误输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
fmt.Println("Please enter your full name: ")
fmt.Scanln(&firstName, &lastName)
// fmt.Scanf("%s %s", &firstName, &lastName)
fmt.Printf("Hi %s %s!\n", firstName, lastName) // Hi Chris Naegels
fmt.Sscanf(input, format, &f, &i, &s)
fmt.Println("From the string we read: ", f, i, s)
}
// 带缓冲区的IO,提高读写性能
func main() {
inputReader := bufio.NewReader(os.Stdin)
input, err := inputReader.ReadString('\n')
if err == nil {
fmt.Printf("The input was: %s\n", input)
}
}

File

  1. os.File封装所有文件相关操作,os.Stdin,os.Stdout, os.Stderr 都是*os.File
    a. 打开一个文件进行读操作: os.Open(name string) (*File, error)
    b. 关闭一个文件: File.Close()
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
// bufio 读文件
func main() {
inputFile, err := os.Open("input.dat")
if err != nil {
fmt.Printf("open file err:%v\n", err)
return
}
defer inputFile.Close()
inputReader := bufio.NewReader(inputFile)
for {
inputString, readerError := inputReader.ReadString('\n')
if readerError == io.EOF {
return
}
fmt.Printf("The input was: %s", inputString)
}
}
// ioutil 一次性读取整个文件
func main() {
inputFile := "products.txt"
outputFile := "products_copy.txt"
buf, err := ioutil.ReadFile(inputFile)
if err != nil {
fmt.Fprintf(os.Stderr, "File Error: %s\n", err)
return
}
fmt.Printf("%s\n", string(buf))
err = ioutil.WriteFile(outputFile, buf, 0x644)
if err != nil {
panic(err.Error())
}
}
// 读gz压缩文件
import "compress/gzip"
func main() {
fi, err := os.Open("test.zip")
if err != nil {
fmt.Fprintf(os.Stderr, "Can’t open file: error: %s\n", err)
os.Exit(1)
}
fz, err := gzip.NewReader(fi)
if err != nil {
fmt.Fprintf(os.Stderr, "open gzip failed, err: %v\n", err)
return
}
r := bufio.NewReader(fz)
for {
line, err := r.ReadString('\n')
if err != nil {
fmt.Println("Done reading file")
os.Exit(0)
}
fmt.Println(line)
}
}
  • 文件写入

    os.OpenFile(“output.dat”, os.O_WRONLY|os.O_CREATE, 0666)

    1. os.O_WRONLY 只写
    2. os.O_CREATE:创建文件
    3. os.O_RDONLY:只读
    4. os.O_RDWR:读写
    5. os.O_TRUNC :清空

    outputWriter.Flush()

  • 拷贝文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
    return
    }
    defer src.Close()
    dst, err := os.OpenFile(dstName, os.O_WRONLY|os.O_CREATE, 0644)
    if err != nil {
    return
    }
    defer dst.Close()
    return io.Copy(dst, src)
    }

命令行参数

  • os.Args是一个string的切片,用来存储所有的命令行参数.len(os.Args)

  • flag包的使用,用来解析命令行参数:

    1
    2
    3
    4
    flag.BoolVar(&test, "b", false, "require a bool val") // 说明文字
    flag.StringVar(&str, "s", "", "require a str val")
    flag.IntVar(&count, "c", 1001, "require a int val")
    flag.Parse()

Json

  • “encoding/json”
  • 序列化: json.Marshal(data interface{})
  • 反序列化: json.UnMarshal(data []byte, v interface{})

  • json序列化结构体

  • json序列化map

错误处理

  • var errNotFound error = errors.New("Not found error")

  • error是一个接口

  • 自定义错误

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    type error interface {
    Error() string
    }
    type PathError struct {
    Op string
    Path string
    err string
    }
    // 实现error接口
    func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
    }
  • 判断error类型,使用类型断言switch err.(type)

  • Panic&Recover

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    func badCall() {
    panic("bad end")
    }
    func test() {
    // recover 捕获panic
    defer func() {
    if e := recover(); e != nil {
    fmt.Printf("Panicking %s\r\n", e)
    }
    }()
    badCall()
    fmt.Printf("After bad call\r\n")
    }

goroutine和channel

  • 全局变量共享内存,需要加锁

  • go提供的channel机制,实现协程间的数据通信。线程安全的,类型unix的管道,先进先出

    • make申请内存
  • 判断管道是否关闭

    • if data, ok := <- ch , ok{}
    • for range遍历channel,如果channel不关闭,会一直阻塞取数据。channel关闭后,取完就会退出for range
  • 定时器

    t.C是一个大小为1的chan Time类型管道,每隔指定的时间会尝试往其中写入一个时间

    1
    2
    3
    4
    5
    6
    t := time.NewTicker(5 * time.Second)
    for v := range t.C {
    fmt.Println(v)
    }
    // 下面的After也会创建一个chan,但是不会在触发后立即被垃圾回收,存在内存泄露。不建议使用
    <-time.After(5 * time.Second)
  • 判断chan状态以及读取写超时

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    go func() {
    t := time.NewTicker(5 * time.Second)
    for {
    select {
    case num := <-channel:
    fmt.Println(num)
    case <-t.C:
    fmt.Println("超时")
    case <-time.After(3 * time.Second): // 不推荐使用,容易内存泄露
    fmt.Println("超时")
    quit <- true
    }
    t.Stop()
    }
    }()

单元测试

  • 导入testing
  • 文件名以_test.go结尾的为测试用例文件
  • 测试用例函数必须以Test开头, 传入的第一个参数为*testing.T
1
2
3
4
5
6
7
func TestAdd(t *testing.T){
r := add(2, 4)
if(r != 6){
t.Fatalf("add(2, 4) error, expect : %d, got: %d", 6, r)
}
t.Log("test add success")
}
  • 测试 go test -v

socket编程

  1. 服务端的处理流程
    a. 监听端口
    b. 接收客户端的链接
    c. 创建goroutine,处理该链接

  2. 客户端的处理流程
    a. 建立与服务端的链接
    b. 进行数据收发
    c. 关闭链接

  3. 发送http请求

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    response, err := http.Get("http://www.baidu.com")
    if err!= nil {
    log.Println(err)
    }
    body := response.Body
    defer body.Close()
    res := ""
    buf := make([]byte, 2048)
    for {
    n, err:= body.Read(buf)
    log.Println(n)
    if err != nil || n == 0{
    log.Println(err, n)
    break
    }
    res += string(buf[:n])
    }

redis

  1. redis连接库
    使用第三方开源的redis库: "github.com/garyburd/redigo/redis"
    官方文档

  2. 链接redis

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    c, err := redis.Dial("tcp", "localhost:6379")
    if err != nil {
    fmt.Println(err, 1)
    return
    }
    defer c.Close()
    // 认证
    if _, err := c.Do("AUTH", "mima"); err != nil {
    c.Close()
    return
    }
    // 选择db
    if _, err := c.Do("SELECT", 0); err != nil {
    c.Close()
    return
    }
    // 读数据
    if str, err := redis.String(c.Do("GET", "a")); err != nil{
    fmt.Println(err)
    }else {
    fmt.Println(str)
    }
  3. 通用操作接口conn.Do("Command", "key", "value")

    • 读取GET GET key

    • 写入SET

      SET key value [EX seconds] [PX milliseconds] [NX|XX]

      EX seconds – 指定过期时间,单位为秒.
      PX milliseconds – 指定过期时间,单位为毫秒.
      NX – 仅当键值不存在时设置键值.
      XX – 仅当键值存在时设置键值.

      c.Do("SET", "password", "123456", "EX", "10")

    • 批量读、写

      1
      2
      3
      4
      5
      6
      //批量写入读取
      MGET key [key ...]
      MSET key value [key value ...]
      //批量写入读取对象(Hashtable)
      HMSET key field value [field value ...]
      HMGET key field [field ...]
    • 检测key EXISTS key

    • 删除key DEL key [key ...]

    • 设置key过期时间 EXPIRE key seconds

  4. Hash表操作

    1
    2
    _, err = c.Do("HSet", "books", "abc", 100)
    r, err := redis.Int(c.Do("HGet", "books", "abc"))
  5. 批量MSet MGet

    1
    2
    _, err = c.Do("MSet", "abc", 100, "efg", 300)
    rs, err := redis.Ints(c.Do("MGet", "abc", "efg"))
  6. 过期时间 _, err = c.Do("expire", "abc", 10)

  7. 队列操作

    1
    2
    _, err = c.Do("lpush", "book_list", "abc", "ceg", 300)
    r, err := redis.String(c.Do("lpop", "book_list"))

连接池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var pool *redis.Pool
pool := &redis.Pool{
MaxIdle: 16,
MaxActive: 0,
IdleTimeout: 300,
// 连接方式
Dial: func () (redis.Conn, error) {
c, err := redis.Dial("tcp", server)
if err != nil {
return nil, err
}
if _, err := c.Do("AUTH", password); err != nil {
c.Close()
return nil, err
}
if _, err := c.Do("SELECT", db); err != nil {
c.Close()
return nil, err
}
return c, nil
},
}

HTTP

秋招求职经历

  1. 我的求职经历
    1. 1. 华为暑期实习
      1. 投递简历
      2. 性格测试
      3. 在线笔试
      4. 一面
      5. 二面
    2. 2. DJI校招
      1. 投递简历
      2. 在线评测
      3. 在线笔试
    3. 3. 百度SRE校招提前批
      1. 投递简历
      2. 电话面试
      3. 宣讲会
      4. 线下一面
      5. 线下二面
    4. 4. 网易 JAVA开发
      1. 投递简历
      2. 在线评测
      3. 在线笔试
      4. 下线面试(杭州/8.23)
    5. 5. 网易游戏 后台开发SRE
      1. 投简历
    6. 6. 阿里巴巴国际
      1. 投简历
      2. 在线评测
      3. 在线编程 没做
      4. 电话一面
      5. 9.7 在线笔试
    7. 7. 百度云中心
      1. 电话一面
  2. 7.25号之后
    1. 8. 华为优招 (云计算方向)
      1. 直通面试
      2. 技术面 8.17
      3. 综合面试 8.17
    2. 9. 虾皮科技
      1. 简历、内推
      2. 在线评测
    3. 10. 链家 贝壳找房
      1. 投递简历
      2. 在线笔试
      3. 线下面试 北京9.01
    4. 11. 拼多多
    5. 12. 美团
      1. 投递简历
      2. 9.6 笔试
      3. 9.11 光谷线下面试
    6. 13. 携程 云计算工程师
      1. 投递简历 8.11
      2. 在线笔试 9.4
    7. 14.微众银行 后台开发
      1. 投递简历 8.19
    8. 15. 字节跳动 后台开发
      1. 投递简历
      2. 在线笔试 8.25
    9. 16. FreeWheel
      1. 9.3 华科线下笔试
      2. 9.4 华科线下一面与二面
    10. 17. 顺丰科技
      1. 投简历 JAVA研发8.24
    11. 18. 小米科技
      1. 投简历 软件开发工程师 8.24
    12. 19. 腾讯科技
      1. 投简历 8.24 web开发方向
    13. 20.招商银行信用卡中心
      1. 投简历 IT类 开发 2018.08.25
      2. 在线笔试
    14. 21. BliBli
      1. 9.4 电话一面
      2. 9.5 电话二面
      3. 10.15 电话三面
    15. 22. 快手
      1. 9.10 在线笔试
    16. 23. 招银网络科技
    17. 24. Vivo
    18. 25. 同花顺
    19. 26. 新浪
    20. 30. 虎牙 java
    21. 31. keep
      1. 9.10 线下投简历
    22. 32. 网易雷火 运维工程师
    23. 33. BIGO
    24. 34 杭州笛佛软件有限公司
    25. 35 汉德软件
    26. 36 玄武科技
    27. 37 滴滴 后端研发工程师
      1. 9.11 邮箱投递简历
    28. 38 YY欢聚时代
      1. 9.11 邮箱投递简历
      2. 9.16 在线笔试
      3. 9.25 线下一面
    29. 39 4399
      1. 9.11 官网投递Web后端
      2. 9.12 线上笔试
    30. 40 平安智慧城
    31. 41 乐贝 lebbay 与拼多多 同母公司

我的求职经历

1. 华为暑期实习

投递简历

  • 官网投递,有内推
  • java开发、web开发

性格测试

  • 坑爹
  • 很多题
  • 超时了,我中间去取外卖。。。
  • 选的中间值太多,1-9个选项,千万不要选456,否则后面会给你1-5个选项的类似题目。
  • 同一题前后会出现2次,2次选了两个极端的,很容易挂。
  • 如果挂了没有补考,除非你被strong hire,发offer前会补考。

在线笔试

  • 牛客网

  • 5个编程题

    其中有一道是背包问题

一面

  • 没怎么聊项目
  • 做了2个超级简单的编程题
  • 面试官友好
  • 然后直接二面

二面

  • 为啥你的笔试成绩这么差?
  • 为啥你的性格评测没过?
  • 挂掉
  • 面试官不友好

2. DJI校招

投递简历

  • 在线投递,有内推
  • 后台开发

在线评测

  • 价值输出很明显

  • 题目也超级有意思

  • 我很怀疑DJI是在借校招的名义进行调研。

  • 做完在线评测,立刻就想卖身给DJI

  • 很多题目给的选项非常的open,甚至是完全对立的。

    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
    数学问题:
    1. 包含透明方块和不透明方块立方体的三透视图
    2. abcde互相踢球
    3. 三男两女的专业推理问题
    4. 甲乙约会两人相互等待的概率问题
    5. 六宫格放4皇后的可能性问题
    个人工作发展:
    1.工作能力
    2.和领导冲突,
    3.重要项目决策
    4.小领导偷了你的idea
    5.协作项目发现可能存在隐患问题
    6. 你愿意花费在技术和管理的时间比例?
    dji产品问题:
    1. 哪个功能你觉得用的最少,全景,跟拍,指哪飞哪,延迟拍摄
    2. 产品feature和实际需求不一致
    3. dji和小米 华为 GoPro哪家合作更好
    4. dji低端飞行器是否要打上dji的logo
    5. 日本的高端电器产品只在国内销售确不在中国市场销售的原因?
    6. 大公司增加乏力,需要创新,拓展新业务,投入新创新项目的人员和资源占比问题?
    dji发展问题:
    1. 遇到专利流氓
    2. 被美国贸易制裁
    3. 好的公关应该是什么样子的
    隐私和政策问题
    1. 是否实名dji飞行器?
    2.哪种实名实施方法更合理?
    3. 内置sim卡实施汇报位置是否可行
    4.内置sim卡向飞管部门汇报属于监管上的进步还是退步?
    性格测试
    1. 有个人的传奇经历,你是否相信?
    2. 你的孩子(幼儿园)参加学校组织的野外露营,在老师禁止半夜外出时,于夜里三点叫醒小张,去河边玩水,受到惊吓。作为家长你应该。。。

在线笔试

  • 有点low啊
  • 传统的笔试,有很多选择和填空(共60分)
  • 有几个和C++强相关的题我不会
  • 竟然还有2个问答题,个人blog的sql表设计和blog部署;http的6中method。(20分)
  • 最后2个编程题,竟然不是oj 形式,相当于在纸上手撸代码。一个sql查询语句;一个矩阵最后跳到底部的最少跳跃次数问题。(20分)

mark

3. 百度SRE校招提前批

投递简历

  • 邮箱,内推/自推
  • 后台开发,运维开发,云开发

电话面试

  • 聊项目

  • 聊技术

  • 2个编程题, 判断链表有环;100万个数据找出top10大的数

  • linux下怎么查找一个文本文件记录里的ip,我说了grep和awk,

    问我对awk了解多少,我说用的不多,必须看要文档。面试官说你能提到awk就很棒了,然后又说awk很强大,数值计算,函数,文本处理等,需要专门写一本厚书来介绍。

  • 如果客户投诉百度网站响应慢,如何电话指导他排除网络问题

    我说的360傻瓜网络监测工具,ping。

    后来面试官说还可以试试别的网站卡不卡。

    印象深刻。

  • python如何动态改变函数的功能

    我回答了装饰器和咖啡糖,经常使用,自己实现的不多。

  • 对docker熟悉吗?docker的优势在哪里呢。

宣讲会

  • 百度的技术牛逼
  • 通过手机陀螺仪检测恶意流量
  • 搜索query的智能预充,晚上服务器空载时,进行query,第二天用户就可以从缓存中获取查询结果。
  • 智能运维,智能流量监测啥的,好多内容记不住了。

线下一面

  • 聊项目

  • 聊技术

  • 手撸代码

    2G内存,10个1G的文件(key 空格 value),找出现次数最高的top3 key记录,并求该key的value均值。

    没撸出来,我手撸代码效率太低了(主要是心里也有点怵),思路错了,挺尴尬的,没看我的代码,不过我很快给出了hash 分流,面试官说那就是hash桶呀。

  • 给出你估算的武汉市红路灯数量,不管对错,说出理由即可。

    不会啊,然后估算一下武大的井盖有多少,也不会强行过掉。

  • 聊了70分钟,进入二面

线下二面

  • 聊项目

  • 聊技术

  • 问了之前2面的情况

  • 对个人的评价,没有回答好。

  • 智力游戏,100个编号,一次能被1到100整除的人按一下手里的开关,求在40到50之间的开关打开的那个人。

    倒计时2分钟,没搞出来。

    然后说虽然你的思路是对的,但是不是考的计算,这么短的时间内是可以尝试找规律的。

  • 求$a^b$结果的后3位值。

    受上个题目的影响,我以为又要找规律。

    然后给了我5分钟撸代码,过了一半的时间我还没动笔。面试官说这个不是找规律啊,先解决问题。

    然后我写了个while循环的暴力代码。

    面试官问有没有优化的方法,我说了一个将b以二进制划分的方法,面试官说思路是对的。

  • 运维、故障排查、服务优化。

    只能想到啥说啥,并没有成体系的知识结构。

百度的技术大佬人真的超级nice,和他们交流收获很大。

4. 网易 JAVA开发

投递简历

  • 有内推码
  • 投完直接在线测评

在线评测

心态爆炸,全程靠蒙

很多题是考公务员才会遇到的那种逻辑推理。图表分析和数值计算的更是难受。

  • 60分钟,4类题型
  • 阅读理解(10min,10题)
  • 图表分析(10min, 10题)
  • 看图找规律/不同(10,10)
  • 性格测试(30min,90题)

在线笔试

  • 8.11, 4道算法题只AC了1个,不太理想

下线面试(杭州/8.23)

  • 准备中

5. 网易游戏 后台开发SRE

投简历

  • 有内推码
  • 显示已处理,无下文

6. 阿里巴巴国际

投简历

  • 内推/自推

在线评测

在线编程 没做

电话一面

  • 我还没做测评和在线编程,就有面试官约电面
  • 如果电面挂了,是不是测评也不用做T.T
  • 主要是做JAVA web开发
  • 问了下项目,成绩,和JVM。
  • 直接GG

9.7 在线笔试

  • 没有投简历,自动复活的

  • 又要把我捞出来鞭尸么

  • 题目很难 10个选择题,涉及智商检查,逻辑推理、概率、积分、微分、机器学习、L1和L2正则化,我选择go die!!!!。

    2道编程题为附加题,

    • 判断点是否在不规则多边形内部,如果不在,最小的距离是多少?

    • 数组划分的种类

      划分要求:将数组拆成若干个子数组(元素可以不连续),每个子数组长度大于等于3,内部元素保持原始的相对顺序,子数组之间互相没有交集,。子数组内部为等差或等比数列。

7. 百度云中心

电话一面

  • 连续3个整数之积是否一定可以被6整除
  • CAP

7.25号之后


8. 华为优招 (云计算方向)

直通面试

  • 有华为比赛三等奖,可以免笔试

技术面 8.17

  • spring cloud 微服务架构

  • 爬虫

  • 5000台租户虚拟机,监控代理每个上传监控数据的峰值为40kbps。不能直接用kafak收集日志,因为部署身份认证密钥很困难,如何解决?上传协议用什么,上传数据怎么处理?

    涉及到ELK、Kafka、分布式通信,我能想到的就是rest 和消息队列。

    面试官讲他们的做法是,在代理和kafak之间搭建一个消息中间件,使用https协议进行身份认证,然后将https的消息转换成kafka支持的协议,然后由kafka和ELK集群进行后面的数据处理。

  • 技术大佬,人很nice。讲了他们的技术栈,也对我后面的学习提了一些建议。

综合面试 8.17

  • 挂掉了,很伤。估计是我表达不够流利,表现的也不自信。或者是价值观不符,难道是运气太差?!

  • 聊项目,认为我用的技术很简单

  • 你在项目中遇到的困难,如何解决的?

  • 为什么想来华为,想去深圳?

  • 有什么兴趣爱好呢?

  • 对华为公司有什么想了解的?

    • 不要聊生活压力、工作压力、房价之类的
  • 难道我的价值观不符?

    mark

9. 虾皮科技

简历、内推

  • 显示被淘汰,但是又给我发邮件说通过筛选,去做人才评测

在线评测

  • 阅读理解、读表分析、找规律

10. 链家 贝壳找房

mark

投递简历

  • 内推8.14

在线笔试

  • 8.18号

线下面试 北京9.01

  • 凉凉
  • 一面ok
  • 二面时已经晚上7点了, 日狗。手撸代码,有处逻辑写错了,面试官很不耐烦,一直在催我;问到多线程,然后也不明白他想问啥。后来复习时,我猜他想表达的意思应该是CAS。真是人渣啊,
  • 以后再也不去异地面了,劳神伤财,天时地利人和一个都没有。

11. 拼多多

  • 邮箱投递简历
  • 8.5号笔试
  • 笔试没通过

12. 美团

投递简历

mark

9.6 笔试

  • 题目不难

9.11 光谷线下面试

13. 携程 云计算工程师

投递简历 8.11

mark

在线笔试 9.4

  • 题目很简单

14.微众银行 后台开发

投递简历 8.19

mark

15. 字节跳动 后台开发

投递简历

  • 有内推码

mark

在线笔试 8.25

  • 凉凉,宇宙条。

16. FreeWheel

9.3 华科线下笔试

  • 3道算法题,2道不会

  • osi tcp的7层模型, TCP3次握手

  • 线程和进程的区别。

  • 算法题

    1. 给定的树,和某个叶子节点,以叶子节点为根,按照一定的要求重建树。

    2. 两条平行线(y=0.0y=(float) c)之间有一系列的点和一个可以自由运动的圆,圆可以安全通过,即不碰到任意的点或水平线,求圆的最大直径。要求精确度为0.01.

      这个我不会做,行哥给了一个思路:

      • 转化为图模型:将两条水平线视为图的两个Node,记为A、B;将所有的点视为图的节点;

      • 对于给定的直径d,如果d>任意两个节点的距离,则两点间有边,因而可以构建出一个图。

      • 判定:如果从节点A到B存在路径(连通),则水平线内的路是被封死的,因而无法通过。

      • 二分查找:找到最大的直径d,对于每次尝试的直径,构建相应的图。

    3. 集合运算

      mark

![](http://img.shuaiyy.cn/blog/180903/m2DGA6H9hK.png?imageslim)

时间来不及了,没认真思考。

+ 考虑用HashMap + bitmap实现

  HashMap存储满足相应投放规则的广告集合(bitmap),即key为投放规则,bitmap为满足要求的广告uid的合集;对于一个uid,如果满足某个规则,则相应的bitmap的uid的位置设为1,否则设为0;

+ 空间复杂度分析

  Key: 对于两个维度,坐标(loc,m种)和设备(dev,n种),那么共有$m+n+m*n$个key,满足loc的,满足dev的,必须同时满足loc和dev,三种情况。

  value: 假设有2000W个广告,那么需要相应2000Wbit的存储空间,约为2.4MB。

  假设有100个地区和10种设备,需要的内存空间约为2.5GB。

+ 时间复杂度分析

  需要`O(N)`的时间复杂度初始化HashMap

  添加一条广告的记录为`O(1)*k`,k为该广告所满足的规则数。

  针对给定用户`loc=A,dev=B`,查找其匹配的广告:即A、B、AB三种类型的广告集合求并集,

  $O(1)$时间复杂度的bitmap的`&`操作。

  取出匹配的广告的uid,即在bitmap中查找所有为1的位置的下标,时间复杂度为`O(N)`,显然该操作可以通过多线程加速,map/reduce的思想。

9.4 华科线下一面与二面

  • 手撸代码,超级简单的编程题
  • 问了一些基础知识和项目相关的
  • 要求英文自我介绍GG
  • 本来以为过不了笔试的,2道编程大题没做(40分)。
  • 本想交流一下笔试题的,结果两个面试官都说题不是他出的,也不是他改的。

17. 顺丰科技

投简历 JAVA研发8.24

  • 笔试面试很简单
  • 拿到offer
  • 拒绝,工资低,加上顺丰科技员工删数据库事件,对发展前景担忧。

18. 小米科技

投简历 软件开发工程师 8.24

  • 线下笔试 9.15
  • 线下一面和二面 10.16
    • 基础知识
    • 手撸代码
    • 项目经历
    • 只有武汉岗位,放弃

19. 腾讯科技

投简历 8.24 web开发方向

  • 简历未通过

20.招商银行信用卡中心

投简历 IT类 开发 2018.08.25

在线笔试

  • 9.8 题目很简单
  • 面试放弃,时间冲突

21. BliBli

  • 记不清啥时候投的简历和岗位了

9.4 电话一面

  • 聊了一些基础知识,docker,微服务。
  • 然后面试官花了15分钟给我介绍B站的情况
    • 在向技术驱动型公司转型
    • 在探索docker和容器云,并逐步迁移业务至微服务
    • 涉及运维、平台研发等,devops
    • 有BAT的大佬,当前的云部门PaaS很像是内部创业,管理的服务器1000+,为80%的业务提供服务。

9.5 电话二面

  • 二面有点蒙圈
  • 设计一个秒杀系统
    • 我只回答了二级队列
  • 分布式缓存的CAP问题,如何保持计算结果的一致性
  • 手机APP缓存的数据
    • 时间戳

10.15 电话三面

  • 问了gitflow项目开发流程,不会
  • 问我用过哪些Linux系统
  • 如何解决工作中遇到的问题

22. 快手

9.10 在线笔试

  • 笔试未通过

23. 招银网络科技

  • 拿到offer

24. Vivo

  • 投简历
  • 在线笔试,未通过

25. 同花顺

  • 简历未通过

26. 新浪

    1. 8电话一面
    1. 15电话二面

没有下文

30. 虎牙 java

  • 没有下文

31. keep

9.10 线下投简历

  • 之前线上投过简历的
  • 笔试通过
  • 时间冲突,没去面试

32. 网易雷火 运维工程师

  • 线下笔试 9.16
  • 电话一面 9.30
  • 电话二面
  • 电话三面

面试进行的太晚了,估计hc早就没有了。

33. BIGO

东南亚的直播平台,java开发

一面面试通过

二面未过

放弃,因为面试官说后台岗位已满,希望转客户端前端开发,拒绝。

34 杭州笛佛软件有限公司

ERP产品,软件产品——“网店管家”

简历未通过

35 汉德软件

简历未通过

36 玄武科技

  • 笔试未通过

37 滴滴 后端研发工程师

9.11 邮箱投递简历

  • 笔试未通过

38 YY欢聚时代

9.11 邮箱投递简历

9.16 在线笔试

9.25 线下一面

  • 聊的还不错,面试官是做大数据的,爬虫工程应用懂得很多,问的很深入
  • 没有下文
  • 和面试官交流,感觉他们价值观有点问题,不仅爬自己内部的数据,竞争对手的数据,还会向黑客购买数据,还说的理直气壮。

39 4399

9.11 官网投递Web后端

9.12 线上笔试

没有下文

40 平安智慧城

  • 没有笔面试通知

41 乐贝 lebbay 与拼多多 同母公司

  • 一面挂

    现场手撸代码,没做出来。

    是个贪心算法的题目。

��ת�ء�������˼��

�Dz��ó����ˣ����������ļ�ʱ���ֵ�

�Ķ���˼��

ѧ����˼���裬˼����ѧ����

ѧϰ��˼�����ศ���ɵĽ������ϡ�

�࿴����������֪����Ҫ��������ƽʱ���������ڹ۲���˼����ϰ�ߣ����޷������յĸ�����֪ʶ�¹۵����ж���˼�����޷������������Լ��ܽ��ܵĺ���֮����Ҳ�޷�������˵���������ĵط����к������ɣ��Ǿͻ����ִ��ԡ�������������֢״�������㿴����Խ�࣬��������Խ�࣬����Խ��ã��Խ�����ʴӡ�

���ǽ�������ʱ���󲿷�����ѧϰ��·���������ĵط���

��������֦����˵����Ȼ�������ǹ������Ƕ���˼���������ܾ�������˵�ĺ��е������Ҿ�����Ҫ�����������㡣�������ף�����������������֪ʶ������˼άϰ����������֪�����ڱȽϵ͵IJ㼶���޷��Ա������߲㼶����֪�۵���������������

��������վ����άʱ�俴��άƽ�������ῴ��һ���������������������Լ���һ�������ڶ�ά��������ֽƬ�ˣ�����������Ŭ��������������ά�������������ˡ�

Ϊʲô�����ܳ����Ÿ���ҥ�ԣ�Ϊʲô��Щ�����ű��㽻����˰�ġ���ת�����й��ˡ�������������һ����һ��Ⱥ����������Ϊ�󲿷�����ȱ���㹻����֪ʶ������˼ά�������ֱ��Ƿ����١�

��������ָ�������²�˵�����ĺ��ķ�˿�����������˺�С�����꣬�������������Ǹ���ҥ�Ժ͡���ת�����й��ˡ���֧���ߺʹ����ߡ�

��Щ�ر����ױ����������ִ���ʽ����ƭ��ϴ�Ե��ˣ�������֪�����Ƚ������ˡ�

�����з�˿˵�ҵļ���/����/ͬѧ/ͬ��������ij����������ƭ��ϴ�ԣ���ȫ������������Ͷ��ȥ�ˣ���ô˵Ҳ˵��ͨ��������ô�졣

���ڡ����ģ�����һ��ƭ��Χ���������ˣ��ַ����������������������ش����������⣺��ֻ�ܽ��и����������գ��������Ź�򡢷Ƿ��н���

��Ϊ����ʵ������������֮���飬��֮�������ֳ��淽��ֻ����Щ��������ͬ��֪ˮƽ�������ã������ø�������û�ã�Ҫô������Ҫô�������ˣ�Ҫô�������

ǰ�����豻ײ������Ҳ�Ǻ������߰���ʵ����������û�ã� ֻ��һֽ��Ʊ�ĵ����ſڲŹ��á���֪���������µ�����֦�뿴���������豻ײ�������������߶�����һ�µ����½�չͨ������

��ά��������������������ά������ƽ���ˣ���֮ȴ���С���ά�ȵ���Ҫ�͵�ά�ȵ��˽���ֻ�ܱ��Ƚ�ά��

����Ҫ�����Լ�����֪������û��ʲô�ٳɰֻ࣬�н�һ���࿴���룬�����Ƕ࿴����ʱ���������ľ��䣬��������������֪Ч�ʣ���������֪ʶ������ˮ�ء�

����������ˮ�����ˣ�����ʲô��ӱ�����Ĺ۵����֣����Ͳ���һ��һէ�������е����ͻ����յ����ij������һ���Ŵ����Ĵ���������û����ֱ��������Ͱ��


�һ����㣬ֻ�Dz�����������ϵ��
ʱ�����ߣ��������ߡ�
��Щ���������ž���Ҳû��ϵ����Щ��˵���ټ�����Ҳ������
����ʱ��һֱ�ߵ���ô�죬���ǻ᲻�������䶼û��������
˵�õģ������ˡ�
�Һܺã���ô���أ�
��Щ�˕A���֣���Щ�����ˣ���Щ��ȴ����Զ�ǵá�
���磬�ЕA�ˣ��������޻���˵��
������ȴ�޻���˵��
������Ϥ�����ǣ��������������ǡ�
�㻹���㣬�һ����ң�ֻ�DZ���İ���ˡ�
��û����ôһ���ˣ�
��ɾ��ta��ȴ���������б���
Ȼ������Ҳ�������֡�
�����վ�����İ���ˣ�����ϵ�����ټ���
��ʱ������������Ҳֻ����������
��ʱ��ϲ��������Ҳֻ�Dz�֪Ϊ�ε�һ����Ը��
����İ���ģ����������׵�ijij��
�������仰ʱ�����������ij��λ�ã�ͻȻ�Ͳ�����һ�¡�
�㿴�����仰���ʺ����ǰ���
������������һ����ÿ�춼�ظ��š�
����ν�첻���֣��Ѳ��ѹ���
����Ī���ĸ裬����İ�����ˡ�
���ţ����š�
�Dz��ǣ�����һ����Ҫ���ߣ�
����Ҳ�ã�����Ҳ�ա�
û��˭������Զ��˭�����ܻ��ڣ�����Ҫ������
̫�����£�������������ʱ��ȴ���ˣ�������׷���ϵġ�
������������Խ�����Լ��ͱ���Խ��Ĭ��
�������׸質�ģ�Խ����Խ�µ���Խ����Խ������
һ���˵�ʱ�������ᷢ�������������ĺܰ�����
���������˳�ӵ���Ľֵ����Ļ��ǻ��µ���
һ���ˣ�ϰ�߲�ϰ�ߵġ�
������ô������˵����Ҫ����Ե��
�Ż���ʶ���Ż�������
һ���˾��ˣ���Ҳ����ľ�ˡ�
��ʱ���ܻ����ú��ۣ�����Ϣ�������Ρ�
��Ϣʱ��ȴ�ֲ�֪����ʲô�ã�Ȼ����һ���ˡ�
��Ϊ�Ѿ�����ν�ˡ�
�ഺ���������ž͹��ˡ�
�����⣬�ŷ������Ƕ�������������
�ܶ��ºܶ��ˣ����˾��ǹ��ˣ����ٻ�����
���ԣ������ǻ����ᣬ���Լ������ģ����Լ������ģ���ϧ�ɣ�
�������У��˳���ôӵ����ȴ��ô�ա�
Խ������Խ�Ǽ�į�������Ƶ�·�������������У���Щ��į��Щ����������������
�⼸���������꣬����ҲԽ��Խ�䡣
һ���˱����Լ���Ȼ����ο��˵û��ϵ������ȥ�ġ�
����Ҫ�ģ���������������ů������ֻ��һ���򵥵��ʺ���
�����ˣ���ô��ͣ������������;�ķ羰��
������˳�ģ���ô��������Ϣ���úú������еĿ�����
���������ˣ���ô�ͷ��ˣ��������л������������ᷢ�ֺܶ����������㲻�������ġ�
���Լ��õ㣬��Ϊû�˻���ô���㣡
�����ǻ����ᣬ�����Ƕ����ڡ�
���Լ�ϲ�����£����Լ��������ˣ�ȥ�Լ���ȥ�ĵط���
��Ϊ��Ҳ���Ժ�������û�����ˡ�

技术面试题目收集

面试题收集

数据库

  1. 查看查询是否使用了索引

    mark

  2. sql like与索引(后模糊匹配才能让索引有效)

  3. 全文索引与contains函数

  4. 索引的缺点:空间换时间,插入删除索引需要代价,因此需要合理设置索引

  5. Mongodb的数据库优化、MongoDB建立索引

  6. 查询语句优化

  7. 建立索引

    基础索引,单个field索引,组合索引,

  8. 聚集索引和非聚集索引?

  9. 索引类型,哈希,B+树的应用

  10. SVN和Git的区别

    SVN的优点:

    1、采用集中式,易于权限管理,保证安全性;

    2、代码的一致性高;

    3、适合人数不多的项目开发;

    SVN的缺点:

    1、服务器压力太大,

    2、必须连接在服务器上,否则基本不能工作、提交、对比、还原等;

    3、不适合开源开发。

    ===========================

    Git的优点:

    1、适合分布式开发,公共的服务器压力小;

    2、速度快, 成熟的架构,开发灵活;

    3、任意两个开发者之间可以很容易的解决冲突,可以方便的版本回退;

    4、离线工作开发,部署方便。

    5、良好的分支机制,可以让主干代码保持干净。

    Git的缺点:

    1、学习成本比较大,学习周期比较长,要求人员素质比较高;

    2、代码保密性差,一旦开发者把整个库克隆下来就可以完全公开所有代码和版本信息。

Linux:

  1. 十个常用的Linux命令

    查看磁盘空间 df -h

    查看内存free -h cat /proc/meminfo

  2. awk文本分割、查找

阿里面试题:

  1. 视频点播网站文件下载接口

  2. 基础变量/数组写出模拟maven导入包过程

  3. 50个白球50个红球,两个盒子,怎么放让人随机在一个盒子里抽到红球概率最高

  4. 有5个强盗分100枚金币,从1号开始进行分配,必须有半数服从分配才通过,否则1被毙掉,由2号开始分,问1号要怎样分才能使自己利益最大化? 要从后向前看。

  5. n个数里取两个和为s的数

  6. HashMap原理,自定义类型可以作为key吗? rehash过程

  7. HashMap和TreeMap、HashTable的区别

  8. java内存模型 , 垃圾回收算法,GC中可达性分析法,和引用计数法有什么不同?引用计数法有什么问题

  9. JVM类加载机制

  10. 排序算法种类,以及快排的优化 , Array.sort的排序方法,快速排序和堆排序的优缺点

  11. Java多线程实现方式,多线程的同步措施,线程与进程的区别

  12. 操作系统同步方式、通信方式

  13. 计算机网络三次握手四次分手, wait_time的三种差别

  14. Http Get和Post差别

  15. https原理和加密过程,验证证书有效性

  16. Java 64位指针的压缩

  17. Java中锁的实现机制,种类

  18. Java线程池达到提交上限的具体情况

  19. Java如何定位内存泄露?

  20. 如何分析算法的时间复杂度?

  21. 如何判断链表是否有环路

  22. 数组中Arrays.sort的排序方法是什么?

  23. 快速排序和堆排序的优缺点

  24. 二分查找

  25. 堆排序

  26. 说一说GC算法。

  27. 怎么检测死锁。

  28. 说一说ThreadLocal关键字。

  29. 聚簇索引和非聚簇索引的区别。

  30. 线程池

JAVA

集合

  1. Java集合有哪些?

  2. ArrayList、Vector 以及 LinkedList的区别

    ArrayList的默认初始长度是10

  3. map, Hashmap和HashTable的区别

  4. hashMap的底层数据结构,原理实现,如何扩容?

  5. String的内存模型

  6. Collection框架的结果

设计模式

  1. 有哪些设计模式
  2. 实现单例模式的几种方式
  3. 实现代理模式、工厂模式
  4. 观察者模式

线程和并发

  • sleep让出cpu时间,但不放弃同步锁。wait放弃锁,线程进入等待notify唤醒。

  • volatile修饰的list是否是线程安全的?

    不安全,修饰的是引用的可见性。

    Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类。

  • 创建线程的几种方法

  • 线程同步的方式有哪些

  • Executor线程池用法

  • 同步和异步的区别

  • 线程安全的单例模式

  • 如何实现Java线程池

  • ThreadLocal,ThreadLocalMap和HashMap的区别

  • ConcurrentHashMap的源码实现

  • Java进程间通信

  • 对项目中的高并发如何解决?

虚拟机

  • JVM分区

  • Myclass = new Myclass()在jvm中的内存分配

  • 堆分区,新生代的GC动作如何触发?

  • 垃圾回收时,哪些可以对象作为GCRoot?

  • 单例模式

  • OOM的例子,如何导致OOM

    堆上的OOM

    mark

  • 如何导致持久代内存溢出

  • 如何避免内存泄露

  • Java内存泄露和垃圾回收机制

  • Tomcat默认的GC回收方式?

框架

  1. Mybatis或Hibernate的原理

  2. Servlet生命周期

  3. Spring依赖注入的原理

  4. Spring AOP是如何实现的

  5. Spring的启动初始化流程和依赖注入流程, 互相依赖是怎么处理的?

  6. SpringMVC的工作流程

    mark

  7. Spring的线程池

  8. Spring的事务隔离和传播机制

  9. Maven的作用

  10. Tomcat的架构和请求处理流程

  11. redis源码阅读

  12. 负载均衡解决方案

  13. 缓存解决方案

  14. memcache的一致性hash

算法

  1. 有哪些排序算法?复杂度和稳定性

    • 快排

      mark

  2. 红黑树和平衡二叉树

编程题

  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
    35
    36
    public class findMaxSum {
    public int FindGreatestSumOfSubArray(int[] array) {
    if(array == null || (array.length == 1 && array[0] <= 0))
    return 0;
    int cur = array[0];
    int sum = array[0];
    for(int i = 1;i < array.length;i++){
    if(cur < 0)
    cur = 0;
    cur = cur + array[i];
    if(sum <= cur)
    sum = cur;
    }
    return sum;
    }
    //用动态规划
    public int FindGreatestSumOfSubArray2(int[] arr,int n){
    int sum = arr[0];
    int max = arr[0];
    for(int i = 1; i < n; i++){
    sum = getMax(sum+arr[i],arr[i]);
    if(sum >= max)
    max = sum;
    }
    return max;
    }
    public int getMax(int a,int b){
    return a > b ? a: b;
    }
    }

  2. 反转链表

  3. 用两个栈实现队列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
    * 入队只进stack1, 出队只从stack2 pop
    * stack2为空时,从stack1 取出数据压入 stack2。
    **/
    public class StackQueue {
    Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>();
    public void push(int node){
    stack1.push(node);
    }
    public int pop(){
    if(stack2.empty()){
    while(!stack1.empty())
    stack2.push(stack1.pop());
    }
    return stack2.pop();
    }

  4. 字符的集合
    题目描述:输入一个字符串,求出该字符串包含的字符集合

    输入描述:每组数据输入一个字符串,字符串最大长度为100,且只包含字母,不可能为空串,区分大小写。

    输出描述:每组数据一行,按字符串原有的字符顺序,输出字符集合,即重复出现并靠后的字母不输出。

    1
    2
    3
    4
    /**
    * 1. 使用字典判断是否重复出现
    * 2. 使用列表记录第一次出现的key值顺序
    **/

  5. (美团)二维数组打印 题目描述:

    有一个二维数组(n*n),写程序实现从右上角到左下角沿主对角线方向打印。

    给定一个二位数组arr及题目中的参数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
    /**
     *  题意很简单,主要是边界的处理:
     *   1. 沿着主对角线打印,所以每次打印之后x与y都要加1,直到x或y超出边界。
     *   2. 每轮遍历的起始点,是遵循(0,n-1)...(0,0)...(n-1,0)。
      *           也就是y先减小到0,然后x增加到n-1。所以遍历的终止条件是startX>=n。 *
     **/
    import java.util.*;
     
    public class Printer {
        public int[] arrayPrint(int[][] arr, int n) {
            // write code here
            int[] res = new int[n*n];
            int index = 0;
            int startX = 0;
            int startY = n-1;
            while(startX<n){
                int x = startX;
                int y = startY;
                while(x<n&&y<n)
                    res[index++]=arr[x++][y++];
                if(startY>0)
                    startY--;
                else
                    startX++;
            }
            return res;
        }
    }

  6. (去哪儿)表达式合法判断

    题目描述:
    写一段代码,判断一个包括’{‘,’[‘,’(‘,’)’,’]’,’}’的表达式是否合法(注意看样例的合法规则。)
    给定一个表达式A,请返回一个bool值,代表它是否合法。

  7. (奇虎360)挑选镇长

    题目描述:
    360员工桂最近申请了一个长假,一个人背着包出去自助游了。
    路上,他经过了一个小镇,发现小镇的人们都围在一棵树下争吵。桂上前询问情况,得知小镇的人们正缺一个镇长,他们希望能选一个知名又公正的镇长,即,大家希望能选出一个人,所有人都认识他,但同时他不认识镇上除自己以外的其他人(在此,我们默认每个人自己认识自己)。可是小镇里的人太多了,一下子大家谁也说服不了谁。
    “这简单啊。”桂表示。于是他一下子统计出来了镇上人们相互之间的认识关系,并且一下子找到了合适的镇长人选。
    现在你手上也拿到了这样一份认识关系的清单。其中上面给出的认识关系是单向的,即,A认识B与B认识A是相互独立的,只给出A认识B就不能认为B认识A,例如,我认识你,你不一定认识我。而且,这里的认识关系也不具有传递性,即,A认识B,B认识C,但这不代表A认识C。同时,为了方便处理,这份清单中,镇上的N个人依次编号为1到N。你能否像桂一样快速找到合适的镇长人选呢?

    输入描述:
    首先一个正整数T(T≤20),表示数据组数。
    之后每组数据的第一行有2个整数n 和m (1≤n≤105 ,0≤m≤3×105 ),依次表示镇上的人数和相互之间的认识关系数。
    之后m行,第 i 行每行两个数Ai和Bi (1≤Ai ,Bi ≤n ),表示Ai认识Bi。(保证没有重复的认识关系,但可能存在自己认识自己的认识关系)
    保证所有数据中80%的数据满足n≤1000,m≤10000

    输出描述:
    一共2T 行,每组数据对应2行。
    第一行,一个整数,表示你所找出来的合适的镇长人选人数num i 。
    第二行,num i 个整数,每两个数中间用空格隔开,表示你所选的合适的镇长的编号。
    特别的,如果并没有找到合适的镇长,第一行输出一个数0,第二行留空即可(参见样例)。

  8. (华为)字符集合

    题目描述:
    输入一个字符串,求出该字符串包含的字符集合

    输入描述:
    每组数据输入一个字符串,字符串最大长度为100,且只包含字母,不可能为空串,区分大小写。

    输出描述:
    每组数据一行,按字符串原有的字符顺序,输出字符集合,即重复出现并靠后的字母不输出。

  9. 1 1 1 2 3的所有组合

  10. m以内的素数

  11. 递归求阶乘

LeetCode Python题解

已经放弃使用Python,转向Java了

  • Python的语言特性使得很多问题有简洁、巧妙却不通用的解法
  • Python的性能低,很多答案无法通过OJ的时间限制
  • 我参考的算法书给出的是Java代码

LeetCode刷题之旅

1. Two Sum

  • Difficulty: Easy

Given an array of integers, return indices of the two numbers such that they add up to a specific target.

You may assume that each input would have exactly one solution, and you may not use the same element twice.

Example:

1
2
3
4
Given nums = [2, 7, 11, 15], target = 9,
Because nums[0] + nums[1] = 2 + 7 = 9,
return [0, 1].

Solution:

1
2
3
4
5
6
7
8
9
10
11
class Solution(object):
def twoSum(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
for i in range(len(nums)-1):
for j in range(i+1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]

Better Solution:

1
2
3
4
5
6
7
8
9
10
class Solution(object):
def twoSum(self, nums, target):
if len(nums) <= 1:
return False
buff_dict = {}
for i in range(len(nums)):
if nums[i] in buff_dict:
return [buff_dict[nums[i]], i]
else:
buff_dict[target - nums[i]] = i

2.Add Two Numbers

You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order and each of their nodes contain a single digit. Add the two numbers and return it as a linked list.

You may assume the two numbers do not contain any leading zero, except the number 0 itself.

Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)
Output: 7 -> 0 -> 8

两个用链表表示的逆序整数求和

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
# Definition for singly-linked list.
# class ListNode(object):
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution(object):
def addTwoNumbers(self, l1, l2):
"""
:type l1: ListNode
:type l2: ListNode
:rtype: ListNode
"""
x1 = 0
x2 = 0
s1 = []
s2 = []
ans = []
while(l1):
s1.append(l1.val)
l1 = l1.next
while(l2):
s2.append(l2.val)
l2 = l2.next
for i in s1[::-1]:
x1 = x1 * 10 + i
for i in s2[::-1]:
x2 = x2*10 + i
x3 = x1 + x2
if x3<10:
return ListNode(x3)
while(x3 > 0):
ans.append(ListNode(x3 % 10))
x3 = x3 // 10
for i in range(len(ans)-1):
ans[i].next = ans[i+1]
return ans[0]
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
# Definition for singly-linked list.
# class ListNode(object):
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution(object):
def addTwoNumbers(self, l1, l2):
"""
:type l1: ListNode
:type l2: ListNode
:rtype: ListNode
"""
l = p = ListNode(0)
inc = 0
while(l1 or l2):
if not l1:
q = ListNode(l2.val + inc)
l2 = l2.next
elif not l2:
q = ListNode(l1.val + inc)
l1 = l1.next
else:
q = ListNode(l1.val + l2.val + inc)
l1 = l1.next
l2 = l2.next
if q.val > 9:
inc = 1
q.val = q.val % 10
else:
inc = 0
p.next = q
p = p.next
if inc > 0 :
q = ListNode(inc)
p.next = q
p = p.next
return l.next

7.Reverse digits of an integer.

Example1: x = 123, return 321
Example2: x = -123, return -321

Note:
The input is assumed to be a 32-bit signed integer. Your function should return 0 when the reversed integer overflows.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution(object):
def reverse(self, x):
"""
:type x: int
:rtype: int
"""
if x >= 0:
x = int(str(x)[::-1])
else:
x = 0 - int(str(-x)[::-1])
if (x > 0x7FFFFFFF) or (x < -0x80000000):
return 0
return x

9. Palindrome Number

回文数字

1
2
3
4
5
6
7
8
9
10
class Solution(object):
def isPalindrome(self, x):
s = str(x)
if s == s[::-1]:
return True
return False
"""
:type x: int
:rtype: bool
"""

LeetCode Java题解

  1. Awesome Java Leetcode
    1. Easy
    2. Medium
    3. Hard
  2. 001 Two Sum
    1. Description
    2. 思路 0
    3. 思路 1
  3. 002 Add Two Numbers
    1. Description
    2. 思路
  4. 003 Longest Substring Without Repeating Characters
    1. Description
    2. 思路
  5. 004 Median of Two Sorted Arrays
    1. Description
    2. 思路
  6. 005 Longest Palindromic Substring
    1. Description
    2. 思路 0
    3. 思路 1
    4. 思路 2
      1. 背景
      2. 分析
  7. 006 ZigZag Conversion
    1. Description
    2. 思路 0
    3. 思路 1
  8. 007 Reverse Integer
    1. Description
    2. 思路
  9. 008 String to Integer (atoi)
    1. Description
    2. 思路
  10. 009 Palindrome Number
    1. Description
    2. 思路 0
    3. 思路 1
  11. 010 Regular Expression Matching
    1. Description
    2. 思路 0
    3. 思路 1
    4. 思路 2
  12. 011 Container With Most Water
    1. Description
    2. 思路
  13. 012 Integer to Roman
    1. Description
    2. 思路
  14. 013 Roman to Integer
    1. Description
    2. 思路
  15. 014 Longest Common Prefix
    1. Description
    2. 思路
  16. 015 3Sum
    1. Description
    2. 思路
  17. 016 3Sum Closest
    1. Description
    2. 思路
  18. 017 Letter Combinations of a Phone Number
    1. Description
    2. 思路 0
    3. 思路 1
  19. 018 4Sum
    1. Description
    2. 思路 0
    3. 思路 1
  20. 019 Remove Nth Node From End of List
    1. Description
    2. 思路
  21. 020 Valid Parentheses
    1. Description
    2. 思路
  22. 021 Merge Two Sorted Lists
    1. Description
    2. 思路
  23. 022 Generate Parentheses
    1. Description
    2. 思路 0
    3. 思路 1
  24. 023 Merge k Sorted Lists
    1. Description
    2. 思路 0
    3. 思路 1
  25. 024 Swap Nodes in Pairs
    1. Description
    2. 思路 0
    3. 思路 1
  26. 025 Reverse Nodes in k-Group
    1. Description
    2. 思路
  27. 026 Remove Duplicates from Sorted Array
    1. Description
    2. 思路
  28. 027 Remove Element
    1. Description
    2. 思路
  29. 028 Implement strStr()
    1. Description
    2. 思路
  30. 029 Divide Two Integers
    1. Description
    2. 思路
  31. 033 Search in Rotated Sorted Array
    1. Description
    2. 思路
  32. 035 Search Insert Position
    1. Description
    2. 思路
  33. 038 Count and Say
    1. Description
    2. 思路
  34. 043 Multiply Strings
    1. Description
    2. 思路
  35. 044 Wildcard Matching
    1. Description
    2. 思路 0
    3. 思路 1
  36. 049 Group Anagrams
    1. Description
    2. 思路
  37. 050 Pow(x, n)
    1. Description
    2. 思路
  38. 053 Maximum Subarray
    1. Description
    2. 思路 0
    3. 思路 1
  39. 056 Merge Intervals
    1. Description
    2. 思路
  40. 057 Insert Interval
    1. Description
    2. 思路
  41. 058 Length of Last Word
    1. Description
    2. 思路
  42. 066 Plus One
    1. Description
    2. 思路
  43. 067 Add Binary
    1. Description
    2. 思路
  44. 068 Text Justification
    1. Description
    2. 思路
  45. 069 Sqrt(x)
    1. Description
    2. 思路
  46. 070 Climbing Stairs
    1. Description
    2. 思路
  47. 083Remove Duplicates from Sorted List
    1. Description
    2. 思路
  48. 088 Merge Sorted Array
    1. Description
    2. 思路
  49. 100 Same Tree
    1. Description
    2. 思路
  50. 101 Symmetric Tree
    1. Description
    2. 思路 0
    3. 思路 1
  51. 104 Maximum Depth of Binary Tree
    1. Description
    2. 思路
  52. 107 Binary Tree Level Order Traversal II
    1. Description
    2. 思路 0
    3. 思路 1
  53. 108 Convert Sorted Array to Binary Search Tree
    1. Description
    2. 思路
  54. 110 Balanced Binary Tree
    1. Description
    2. 思路
  55. 111 Minimum Depth of Binary Tree
    1. Description
    2. 思路 0
    3. 思路 1
  56. 112 Path Sum
    1. Description
    2. 思路
  57. 118 Pascal’s Triangle
    1. Description
    2. 思路
  58. 119 Pascal’s Triangle II
    1. Description
    2. 思路
  59. 121 Best Time to Buy and Sell Stock
    1. Description
    2. 思路
  60. 122 Best Time to Buy and Sell Stock II
    1. Description
    2. 思路
  61. 543 Diameter of Binary Tree
    1. Description
    2. 思路
  62. 554 Brick Wall
    1. Description
    2. 思路

Awesome Java Leetcode

镇楼诗:

明有科举八股,今有 LeetCode。
八股定格式而取文采心意,LeetCode 定题目且重答案背诵。
美其名曰:”practice makes perfect.”
为何今不如古?
非也非也,
科举为国取士,LeetCode 为 Google 筛码工,各取所需也。

Easy

# Title Tag
1 Two Sum Array, Hash Table
7 Reverse Integer Math
9 Palindrome Number Math
13 Roman to Integer Math, String
14 Longest Common Prefix String
20 Valid Parentheses Stack, String
21 Merge Two Sorted Lists Linked List
26 Remove Duplicates from Sorted Array Array, Two Pointers
27 Remove Element Array, Two Pointers
28 Implement strStr() Two Pointers, String
35 Search Insert Position String
38 Count and Say String
53 Maximum Subarray Array, Divide and Conquer, Dynamic Programming
58 Length of Last Word String
66 Plus One Array, Math
67 Add Binary Math, String
69 Sqrt(x) Binary Search, Math
70 Climbing Stairs Dynamic Programming
83 Remove Duplicates from Sorted List Linked List
88 Merge Sorted Array Array, Two Pointers
100 Same Tree Tree, Depth-first Search
101 Symmetric Tree Tree, Depth-first Search, Breadth-first Search
104 Maximum Depth of Binary Tree Tree, Depth-first Search
107 Binary Tree Level Order Traversal II Tree, Breadth-first Search
108 Convert Sorted Array to Binary Search Tree Tree, Depth-first Search
110 Balanced Binary Tree Tree, Depth-first Search
111 Minimum Depth of Binary Tree Tree, Depth-first Search, Breadth-first Search
112 Path Sum Tree, Depth-first Search
118 Pascal’s Triangle Array
119 Pascal’s Triangle II Array
121 Best Time to Buy and Sell Stock Array, Dynamic Programmin
122 Best Time to Buy and Sell Stock II Array, Greedy
543 Diameter of Binary Tree Tree

Medium

# Title Tag
2 Add Two Numbers Linked List, Math
3 Longest Substring Without Repeating Characters Hash Table, Two Pointers, String
5 Longest Palindromic Substring String
6 ZigZag Conversion String
8 String to Integer (atoi) Math, String
11 Container With Most Water Array, Two Pointers
12 Integer to Roman Math, String
15 3Sum Array, Two Pointers
16 3Sum Closest Array, Two Pointers
17 Letter Combinations of a Phone Number String, Backtracking
18 4Sum Array, Hash Table, Two Pointers
19 Remove Nth Node From End of List Linked List, Two Pointers
22 Generate Parentheses String, Backtracking
24 Swap Nodes in Pairs Linked List
29 Divide Two Integers Math, Binary Search
33 Search in Rotated Sorted Array Arrays, Binary Search
43 Multiply Strings Math, String
49 Group Anagrams Hash Table, String
50 Pow(x, n) Math, Binary Search
56 Merge Intervals Array, Sort
554 Brick Wall Hash Table

Hard

# Title Tag
4 Median of Two Sorted Arrays Array, Binary Search, Divide and Conquer
10 Regular Expression Matching String, Dynamic Programming, Backtracking
23 Merge k Sorted Lists Linked List, Divide and Conquer, Heap
25 Reverse Nodes in k-Group Linked List
44 Wildcard Matching String, Dynamic Programming, Backtracking, Greedy
57 Insert Interval Array, Sort
68 Text Justification String

001 Two Sum

Description

Given an array of integers, return indices of the two numbers such that they add up to a specific target.

You may assume that each input would have exactly one solution, and you may not use the same element twice.

Example:

1
2
3
4
Given nums = [2, 7, 11, 15], target = 9,
Because nums[0] + nums[1] = 2 + 7 = 9,
return [0, 1].

Tags: Array, Hash Table

思路 0

题意是让你从给定的数组中找到两个元素的和为指定值的两个索引,最容易的当然是循环两次,复杂度为 O(n^2),首次提交居然是 2ms,打败了 100% 的提交,谜一样的结果,之后再次提交就再也没跑到过 2ms 了。

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
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 (nums[i] + nums[j] == target) {
return new int[]{i, j};
}
}
}
return null;
}
}

思路 1

利用 HashMap 作为存储,键为目标值减去当前元素值,索引为值,比如 i = 0 时,此时首先要判断 nums[0] = 2 是否在 map 中,如果不存在,那么插入键值对 key = 9 - 2 = 7, value = 0,之后当 i = 1 时,此时判断 nums[1] = 7 已存在于 map 中,那么取出该 value = 0 作为第一个返回值,当前 i 作为第二个返回值,具体代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public int[] twoSum(int[] nums, int target) {
int len = nums.length;
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < len; ++i) {
if (map.containsKey(nums[i])) {
return new int[]{map.get(nums[i]), i};
}
map.put(target - nums[i], i);
}
return null;
}
}

002 Add Two Numbers

Description

You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order and each of their nodes contain a single digit. Add the two numbers and return it as a linked list.

You may assume the two numbers do not contain any leading zero, except the number 0 itself.

Example

1
2
3
Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)
Output: 7 -> 0 -> 8
Explanation: 342 + 465 = 807.

Tags: Linked List, Math

思路

题意我也是看了好久才看懂,就是以链表表示一个数,低位在前,高位在后,所以题中的例子就是 342 + 465 = 807,所以我们模拟计算即可。

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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode node = new ListNode(0);
ListNode n1 = l1, n2 = l2, t = node;
int sum = 0;
while (n1 != null || n2 != null) {
sum /= 10;
if (n1 != null) {
sum += n1.val;
n1 = n1.next;
}
if (n2 != null) {
sum += n2.val;
n2 = n2.next;
}
t.next = new ListNode(sum % 10);
t = t.next;
}
if (sum / 10 != 0) t.next = new ListNode(1);
return node.next;
}
}

003 Longest Substring Without Repeating Characters

Description

Given a string, find the length of the longest substring without repeating characters.

Examples:

Given "abcabcbb", the answer is "abc", which the length is 3.

Given "bbbbb", the answer is "b", with the length of 1.

Given "pwwkew", the answer is "wke", with the length of 3. Note that the answer must be a substring, "pwke" is a subsequence and not a substring.

Tags: Hash Table, Two Pointers, String

思路

题意是计算不带重复字符的最长子字符串的长度,开辟一个 hash 数组来存储该字符上次出现的位置,比如 hash[a] = 3 就是代表 a 字符前一次出现的索引在 3,遍历该字符串,获取到上次出现的最大索引(只能向前,不能退后),与当前的索引做差获取的就是本次所需长度,从中迭代出最大值就是最终答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public int lengthOfLongestSubstring(String s) {
int len;
if (s == null || (len = s.length()) == 0) return 0;
int preP = 0, max = 0;
int[] hash = new int[128];
for (int i = 0; i < len; ++i) {
char c = s.charAt(i);
if (hash[c] > preP) {
preP = hash[c];
}
int l = i - preP + 1;
hash[c] = i + 1;
if (l > max) max = l;
}
return max;
}
}

004 Median of Two Sorted Arrays

Description

There are two sorted arrays nums1 and nums2 of size m and n respectively.

Find the median of the two sorted arrays. The overall run time complexity should be O(log (m+n)).

Example 1:

1
2
3
4
nums1 = [1, 3]
nums2 = [2]
The median is 2.0

Example 2:

1
2
3
4
nums1 = [1, 2]
nums2 = [3, 4]
The median is (2 + 3)/2 = 2.5

Tags: Array, Binary Search, Divide and Conquer

思路

题意是给你两个已排序的递增数组,让你找出其中位数。

乍一看这题并不是很难,因为两序列有序,所以我们很容想到时间复杂度为 O(m + n) 的做法:依次取出两数组中较小的元素,然后找到中间的元素即可。但这题要求的时间复杂度为 O(log(m + n)),那么我们自然而然地就能想到二分查找法进行求解。

题目是让找两数组的中位数,我们可以泛化为求两数组中第 k 大的元素,那么,求中位数就是其中的一个特例而已。helper 函数所起到的作用就是求两数组中第 k 大的元素,下面来解释其原理:

假设数组分别记为 AB,当前需要搜索第 k 大的数,于是我们可以考虑从数组 A 中取出前 m 个元素,从数组 B 中取出前 k - m 个元素。由于数组 AB 分别排序,则 A[m - 1] 大于从数组 A 中取出的其他所有元素,B[k - m - 1] 大于数组 B 中取出的其他所有元素。此时,尽管取出元素之间的相对大小关系不确定,但 A[m - 1]B[k - m - 1] 的较大者一定是这 k 个元素中最大的。那么,较小的那个元素一定不是第 k 大的,这里留给读者自己想象。

为叙述方便,假设 A[m - 1] 是较小的那个元素,那么我们可以把 A[0]A[1]A[m - 1] 排除掉,并且更新 k 值为 k - m,也就是下一次就是从剩余的元素中寻找第 k - m 大的元素,这样,我们就完成了一次范围缩小,同理进行下一轮的操作。

那么什么时候停止操作呢?分两种情况:

  1. 当某个数组的数都被取完了,那么直接返回另一个数组的后 k 个元素即可。

  2. k = 1 时,也就是只需再找一个数即可,也就是取两者当前较小的那个即可。

特别地,我们选取 m = k / 2,下面是我画的草图,希望能帮助大家理解。

借助上面的理论,你能写出相关代码了吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int len = nums1.length + nums2.length;
if (len % 2 == 0) {
return (helper(nums1, 0, nums2, 0, len / 2) + helper(nums1, 0, nums2, 0, len / 2 + 1)) / 2.0;
}
return helper(nums1, 0, nums2, 0, (len + 1) / 2);
}
private int helper(int[] nums1, int m, int[] nums2, int n, int k) {
if (m >= nums1.length) return nums2[n + k - 1];
if (n >= nums2.length) return nums1[m + k - 1];
if (k == 1) return Math.min(nums1[m], nums2[n]);
int p1 = m + k / 2 - 1;
int p2 = n + k / 2 - 1;
int mid1 = p1 < nums1.length ? nums1[p1] : Integer.MAX_VALUE;
int mid2 = p2 < nums2.length ? nums2[p2] : Integer.MAX_VALUE;
if (mid1 < mid2) {
return helper(nums1, m + k / 2, nums2, n, k - k / 2);
}
return helper(nums1, m, nums2, n + k / 2, k - k / 2);
}
}

005 Longest Palindromic Substring

Description

Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.

Example:

1
2
3
4
5
Input: "babad"
Output: "bab"
Note: "aba" is also a valid answer.

Example:

1
2
3
Input: "cbbd"
Output: "bb"

Tags: String

思路 0

题意是寻找出字符串中最长的回文串,所谓回文串就是正序和逆序相同的字符串,也就是关于中间对称。我们先用最常规的做法,依次去求得每个字符的最长回文,要注意每个字符有奇数长度的回文串和偶数长度的回文串两种情况,相信你可以很轻易地从如下代码中找到相关代码,记录最长回文的始末位置即可,时间复杂度的话,首先要遍历一遍字符串,然后对每个字符都去求得最长回文,所以时间复杂度为 O(n^2)

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
class Solution {
int st, end;
public String longestPalindrome(String s) {
int len = s.length();
if (len <= 1) return s;
char[] chars = s.toCharArray();
for (int i = 0; i < len; i++) {
helper(chars, i, i);
helper(chars, i, i + 1);
}
return s.substring(st, end + 1);
}
private void helper(char[] chars, int l, int r) {
while (l >= 0 && r < chars.length && chars[l] == chars[r]) {
--l;
++r;
}
if (end - st < r - l - 2) {
st = l + 1;
end = r - 1;
}
}
}

思路 1

如果利用暴力法遍历所有字串是否回文的情况这道题肯定是 Time Limit Exceeded 的,那么我们是否可以把之前遍历的结果利用上呢,那么动态规划的想法就呼之欲出了,我们定义 dp[i][j] 的意思为字符串区间 [i, j] 是否为回文串,那么我们分三种情况:

  1. i == j 时,那么毫无疑问 dp[i][j] = true

  2. i + 1 == j 时,那么 dp[i][j] 的值取决于 s[i] == s[j]

  3. i + 1 < j 时,那么 dp[i][j] 的值取决于 dp[i + 1][j - 1] && s[i] == s[j]

根据以上的动态转移方程,我们的问题即可迎刃而解,时间复杂度的话显而易见,也是 O(n^2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public String longestPalindrome(String s) {
int len = s.length();
if (len <= 1) return s;
int st = 0, end = 0;
char[] chars = s.toCharArray();
boolean[][] dp = new boolean[len][len];
for (int i = 0; i < len; i++) {
dp[i][i] = true;
for (int j = 0; j < i; j++) {
if (j + 1 == i) {
dp[j][i] = chars[j] == chars[i];
} else {
dp[j][i] = dp[j + 1][i - 1] && chars[j] == chars[i];
}
if (dp[j][i] && i - j > end - st) {
st = j;
end = i;
}
}
}
return s.substring(st, end + 1);
}
}

思路 2

马拉车算法(Manacher’s Algorithm)

背景

给定一个字符串,求出其最长回文子串(回文字符串就是从左到右读和从右往左读完全一样,也就是字符串关于中间对称)。例如:

  1. s = “babad”,最长回文长度为 3,可以是 bab 或者 aba

  2. s = “cbbda”,最长回文长度为 2,即 bb

  3. s = “abcde”,最长回文长度为 1,即单个字符本身。

这个问题等同于 LeetCode 上的 Longest Palindromic Substring,其相关题解可以查看这里:传送门

以上问题的传统思路大概是遍历每一个字符,以该字符为中心向两边查找,其时间复杂度为 O(n^2),效率很差。

1975 年,一个叫 Manacher 的人发明了 Manacher 算法(中文名:马拉车算法),该算法可以把时间复杂度提升到 O(n),下面我以我理解的思路来讲解其原理。

分析

由于回文串的奇偶行不确定,比如 lol 是奇回文,而 lool 是偶回文,马拉车算法的第一步就是对其进行预处理,做法就是在每个字符两侧都加上一个特殊字符,一般就是不会出现在原串中的即可,我们可以选取 #,那么

1
2
lol -> #l#o#l#
lool -> #l#o#o#l#

这样处理后,不管原来字符串长度是奇数还是偶数,最终得到的长度都将是奇数,从而能把两种情况合并起来一起考虑,记预处理后的字符串为 str

我们把一个回文串中最左或最右位置的字符与其对称轴的距离称为回文半径。

马拉车算法定义了一个回文半径数组 len,用 len[i] 表示以第 i 个字符为对称轴的回文串的回文半径,比如以 str[i] 为中心的最长回文串是 str[l, r],那么 len[i] = r - i + 1

我们以 lollool 为例,参看下表。

str # l # o # l # l # o # o # l #
len[] 1 2 1 4 l 2 5 2 1 2 5 2 1 2 1

可以发现 len[i] - 1 就等于该回文串在原串中的长度。

证明:在转换后的字符串 str 中,那么对于以 str[i] 为中心的最长回文串的长度为 2 * len[i] - 1,其中又有 len[i] 个分隔符,所以在原字符串中的回文串长度就是 len[i] - 1

那么我们剩下的工作就是求 len 数组。

为了防止数组越界,我们在首位再加上非 # 的不常用字符,比如 ~,那么 lollool 就表示为 ~#l#o#l#l#o#o#l#~,这样我们就省去写很多 if else 的边界处理。

我们先看一张图,如下所示:

006 ZigZag Conversion

Description

The string "PAYPALISHIRING" is written in a zigzag pattern on a given number of rows like this: (you may want to display this pattern in a fixed font for better legibility)

1
2
3
P A H N
A P L S I I G
Y I R

And then read line by line: "PAHNAPLSIIGYIR"

Write the code that will take a string and make this conversion given a number of rows:

1
string convert(string text, int nRows);

convert("PAYPALISHIRING", 3) should return "PAHNAPLSIIGYIR".

Tags: String

思路 0

题意是让你把字符串按波浪形排好,然后返回横向读取的字符串。

听不懂的话,看下面的表示应该就明白了:

1
2
3
4
5
6
7
0 2n-2 4n-4
1 2n-3 2n-1 4n-5 4n-5
2 2n-4 2n 4n-6 .
. . . . .
. n+1 . 3n-1 .
n-2 n 3n-4 3n-2 5n-6
n-1 3n-3 5n-5

那么我们可以根据上面找规律,可以看到波峰和波谷是单顶点的,它们周期是 2 * (n - 1),单独处理即可;中间的部分每个周期会出现两次,规律很好找,留给读者自己想象,不懂的可以结合以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public String convert(String s, int numRows) {
if (numRows <= 1) return s;
int len = s.length();
char[] chars = s.toCharArray();
int cycle = 2 * (numRows - 1);
StringBuilder sb = new StringBuilder();
for (int j = 0; j < len; j += cycle) {
sb.append(chars[j]);
}
for (int i = 1; i < numRows - 1; i++) {
int step = 2 * i;
for (int j = i; j < len; j += step) {
sb.append(chars[j]);
step = cycle - step;
}
}
for (int j = numRows - 1; j < len; j += cycle) {
sb.append(chars[j]);
}
return sb.toString();
}
}

思路 1

另外一种思路就是开辟相应行数的 StringBuilder 对象,然后模拟波浪生成的样子分别插入到相应的 StringBuilder 对象,比较直白简单,具体代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public String convert(String s, int numRows) {
if (numRows <= 1) return s;
int len = s.length();
char[] chars = s.toCharArray();
StringBuilder[] sbs = new StringBuilder[numRows];
for (int i = 0; i < numRows; i++) {
sbs[i] = new StringBuilder();
}
int i = 0;
while (i < len) {
for (int j = 0; j < numRows && i < len; ++j) {
sbs[j].append(chars[i++]);
}
for (int j = numRows - 2; j >= 1 && i < len; --j) {
sbs[j].append(chars[i++]);
}
}
for (i = 1; i < numRows; i++) {
sbs[0].append(sbs[i]);
}
return sbs[0].toString();
}
}

007 Reverse Integer

Description

Given a 32-bit signed integer, reverse digits of an integer.

Example 1:

1
2
Input: 123
Output: 321

Example 2:

1
2
Input: -123
Output: -321

Example 3:

1
2
Input: 120
Output: 21

Note:

Assume we are dealing with an environment which could only hold integers within the 32-bit signed integer range. For the purpose of this problem, assume that your function returns 0 when the reversed integer overflows.

Tags: Math

思路

题意是给你一个整型数,求它的逆序整型数,而且有个小坑点,当它的逆序整型数溢出的话,那么就返回 0,用我们代码表示的话可以求得结果保存在 long 中,最后把结果和整型的两个范围比较即可。

1
2
3
4
5
6
7
8
class Solution {
public int reverse(int x) {
long res = 0;
for (; x != 0; x /= 10)
res = res * 10 + x % 10;
return res > Integer.MAX_VALUE || res < Integer.MIN_VALUE ? 0 : (int) res;
}
}

008 String to Integer (atoi)

Description

Implement atoi to convert a string to an integer.

Hint: Carefully consider all possible input cases. If you want a challenge, please do not see below and ask yourself what are the possible input cases.

Notes: It is intended for this problem to be specified vaguely (ie, no given input specs). You are responsible to gather all the input requirements up front.

Spoilers:

Requirements for atoi:

The function first discards as many whitespace characters as necessary until the first non-whitespace character is found. Then, starting from this character, takes an optional initial plus or minus sign followed by as many numerical digits as possible, and interprets them as a numerical value.

The string can contain additional characters after those that form the integral number, which are ignored and have no effect on the behavior of this function.

If the first sequence of non-whitespace characters in str is not a valid integral number, or if no such sequence exists because either str is empty or it contains only whitespace characters, no conversion is performed.

If no valid conversion could be performed, a zero value is returned. If the correct value is out of the range of representable values, INT_MAX (2147483647) or INT_MIN (-2147483648) is returned.

Tags: Math, String

思路

题意是把一个字符串转为整型,但要注意所给的要求,先去除最前面的空格,然后判断正负数,注意正数可能包含 +,如果之后存在非数字或全为空则返回 0,而如果合法的值超过 int 表示的最大范围,则根据正负号返回 INT_MAXINT_MIN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public int myAtoi(String str) {
int i = 0, ans = 0, sign = 1, len = str.length();
while (i < len && str.charAt(i) == ' ') ++i;
if (i < len && (str.charAt(i) == '-' || str.charAt(i) == '+')) {
sign = str.charAt(i++) == '+' ? 1 : -1;
}
for (; i < len; ++i) {
int tmp = str.charAt(i) - '0';
if (tmp < 0 || tmp > 9)
break;
if (ans > Integer.MAX_VALUE / 10 || ans == Integer.MAX_VALUE / 10 && Integer.MAX_VALUE % 10 < tmp)
return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
else
ans = ans * 10 + tmp;
}
return sign * ans;
}
}

009 Palindrome Number

Description

Determine whether an integer is a palindrome. Do this without extra space.

Spoilers:

Some hints:

Could negative integers be palindromes? (ie, -1)

If you are thinking of converting the integer to string, note the restriction of using extra space.

You could also try reversing an integer. However, if you have solved the problem “Reverse Integer”, you know that the reversed integer might overflow. How would you handle such case?

There is a more generic way of solving this problem.

Tags: Math

思路 0

题意是判断一个有符号整型数是否是回文,也就是逆序过来的整数和原整数相同,首先负数肯定不是,接下来我们分析一下最普通的解法,就是直接算出他的回文数,然后和给定值比较即可。

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public boolean isPalindrome(int x) {
if (x < 0) return false;
int copyX = x, reverse = 0;
while (copyX > 0) {
reverse = reverse * 10 + copyX % 10;
copyX /= 10;
}
return x == reverse;
}
}

思路 1

好好思考下是否需要计算整个长度,比如 1234321,其实不然,我们只需要计算一半的长度即可,就是在计算过程中的那个逆序数比不断除 10 的数大就结束计算即可,但是这也带来了另一个问题,比如 10 的倍数的数 10010,它也会返回 true,所以我们需要对 10 的倍数的数再加个判断即可,代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public boolean isPalindrome(int x) {
if (x < 0 || (x != 0 && x % 10 == 0)) return false;
int halfReverseX = 0;
while (x > halfReverseX) {
halfReverseX = halfReverseX * 10 + x % 10;
x /= 10;
}
return halfReverseX == x || halfReverseX / 10 == x;
}
}

010 Regular Expression Matching

Description

Implement regular expression matching with support for '.' and '*'.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'.' Matches any single character.
'*' Matches zero or more of the preceding element.
The matching should cover the entire input string (not partial).
The function prototype should be:
bool isMatch(const char *s, const char *p)
Some examples:
isMatch("aa", "a") → false
isMatch("aa", "aa") → true
isMatch("aaa", "aa") → false
isMatch("aa", "a*") → true
isMatch("aa", ".*") → true
isMatch("ab", ".*") → true
isMatch("aab", "c*a*b") → true

Tags: String, Dynamic Programming, Backtracking

思路 0

题意是让让你从判断 s 字符串是否正则匹配于 p,这道题和 [Wildcard Matching][044] 很是相似,区别在于 *,通配符的 * 是可以随意出现的,跟前面字符没有任何关系,其作用是可以表示任意字符串;而正则匹配的 * 不能单独存在,前面必须具有一个字符,其意义是表明前面的这个字符个数可以是任意个数,包括 0 个。首先我们用递归的方式来实现,其思路如下:

  • 如果 sp 都为空,那么返回 true

  • 如果 p 的长度为 1,当 s 的长度也为 1,并且他们首位匹配则返回 true,否则返回 false

  • 如果 p 的第二个字符不为 ‘*’,如果 s 为空,那就返回 false,首位匹配则返回递归调用他们去掉首位的子字符串,否则返回 false

  • 如果 p 的第二个字符为 ‘*’,循环当 s 不为空,且首位匹配,如果递归调用是否匹配 s 字符串和 p 去掉前两位的子字符串,则返回 true,否则 s 去掉首字母继续循环;

  • 返回递归调用 s 字符串和 p 去掉前两位的子字符串是否匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public boolean isMatch(String s, String p) {
if (p.isEmpty()) return s.isEmpty();
if (p.length() == 1) {
return s.length() == 1 && (p.charAt(0) == s.charAt(0) || p.charAt(0) == '.');
}
if (p.charAt(1) != '*') {
if (s.isEmpty()) return false;
return (p.charAt(0) == s.charAt(0) || p.charAt(0) == '.')
&& isMatch(s.substring(1), p.substring(1));
}
// match 1 or more preceding element
while (!s.isEmpty() && (p.charAt(0) == s.charAt(0) || p.charAt(0) == '.')) {
if (isMatch(s, p.substring(2))) return true;
s = s.substring(1);
}
// match 0 preceding element
return isMatch(s, p.substring(2));
}
}

思路 1

我们可以把上面的思路更简单化,如下:

  • 如果 sp 都为空,那么返回 true

  • 如果 p 的第二个字符为 *,由于 * 前面的字符个数可以为任意,那么我们先递归调用个数为 0 的情况;或者当 s 不为空,如果他们的首字母匹配,那么我们就递归调用去掉去掉首字母的 s 和完整的 p

  • 如果 p 的第二个字符不为 *,那么我们就老老实实判断第一个字符是否匹配并且递归调用他们去掉首位的子字符串。

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public boolean isMatch(String s, String p) {
if (p.isEmpty()) return s.isEmpty();
if (p.length() > 1 && p.charAt(1) == '*') {
return isMatch(s, p.substring(2))
|| (!s.isEmpty() && (p.charAt(0) == s.charAt(0) || p.charAt(0) == '.')
&& isMatch(s.substring(1), p));
}
return !s.isEmpty() && (p.charAt(0) == s.charAt(0) || p.charAt(0) == '.')
&& isMatch(s.substring(1), p.substring(1));
}
}

思路 2

另一种思路就是动态规划了,我们定义 dp[i][j] 的真假来表示 s[0..i) 是否匹配 p[0..j),通过思路 1,我们可以确定其状态转移方程如下所示:

  • 如果 p[j - 1] == '*', dp[i][j] = dp[i][j - 2] || (pc[j - 2] == sc[i - 1] || pc[j - 2] == '.') && dp[i - 1][j];

  • 如果 p[j - 1] != '*'dp[i][j] = dp[i - 1][j - 1] && (pc[j - 1] == '.' || pc[j - 1] == sc[i - 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
class Solution {
public boolean isMatch(String s, String p) {
if (p.length() == 0) return s.length() == 0;
int sL = s.length(), pL = p.length();
boolean[][] dp = new boolean[sL + 1][pL + 1];
char[] sc = s.toCharArray(), pc = p.toCharArray();
dp[0][0] = true;
for (int i = 2; i <= pL; ++i) {
if (pc[i - 1] == '*' && dp[0][i - 2]) {
dp[0][i] = true;
}
}
for (int i = 1; i <= sL; ++i) {
for (int j = 1; j <= pL; ++j) {
if (pc[j - 1] == '*') {
dp[i][j] = dp[i][j - 2] || (pc[j - 2] == sc[i - 1] || pc[j - 2] == '.') && dp[i - 1][j];
} else {
dp[i][j] = dp[i - 1][j - 1] && (pc[j - 1] == '.' || pc[j - 1] == sc[i - 1]);
}
}
}
return dp[sL][pL];
}
}

011 Container With Most Water

Description

Given n non-negative integers a1, a2, …, an, where each represents a point at coordinate (i, ai). n vertical lines are drawn such that the two endpoints of line i is at (i, ai) and (i, 0). Find two lines, which together with x-axis forms a container, such that the container contains the most water.

Note: You may not slant the container and n is at least 2.

Tags: Array, Two Pointers

思路

题意是给你 a1, a2, …, ann 个数,代表 (i, ai) 坐标,让你从中找两个点与 x 轴围成的容器可以容纳最多的水。

不明白的话可以看数据为 1 8 6 2 5 4 8 3 7 所示的图。

如果用暴力法求每种情况的结果,其时间复杂度为 O(n^2),相信肯定会超时,我们可以探索下是否有更巧妙的办法呢,题目的标签有双指针,是否就可以想到首尾各放一指针,然后根据条件来收缩。首先计算一次首尾构成的最大面积,然后分析下该移动哪个指针,如果移动大的那个指针的话,那样只会减小面积,所以我们要移动小的那个指针,小的那个指针移动到哪呢?当然是移动到大于之前的值的地方,否则面积不都比之前小么,然后继续更新最大值即可,借助如上分析写出如下代码应该不是什么难事了吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public int maxArea(int[] height) {
int l = 0, r = height.length - 1;
int max = 0, h = 0;
while (l < r) {
h = Math.min(height[l], height[r]);
max = Math.max(max, (r - l) * h);
while (height[l] <= h && l < r) ++l;
while (height[r] <= h && l < r) --r;
}
return max;
}
}

012 Integer to Roman

Description

Given an integer, convert it to a roman numeral.

Input is guaranteed to be within the range from 1 to 3999.

Tags: Math, String

思路

题意是整型数转罗马数字,范围从 1 到 3999,查看下百度百科的罗马数字介绍如下:

  • 相同的数字连写,所表示的数等于这些数字相加得到的数,如 Ⅲ=3;

  • 小的数字在大的数字的右边,所表示的数等于这些数字相加得到的数,如 Ⅷ=8、Ⅻ=12;

  • 小的数字(限于 Ⅰ、X 和 C)在大的数字的左边,所表示的数等于大数减小数得到的数,如 Ⅳ=4、Ⅸ=9。

那么我们可以把整数的每一位分离出来,让其每一位都用相应的罗马数字位表示,最终拼接完成。比如 621 我们可以分离百、十、个分别为 621,那么 600 对应的罗马数字是 DC20 对应的罗马数字是 XX1 对应的罗马数字是 I,所以最终答案便是 DCXXI

1
2
3
4
5
6
7
8
9
class Solution {
public String intToRoman(int num) {
String M[] = {"", "M", "MM", "MMM"};
String C[] = {"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"};
String X[] = {"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"};
String I[] = {"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"};
return M[num / 1000] + C[(num % 1000) / 100] + X[(num % 100) / 10] + I[num % 10];
}
}

013 Roman to Integer

Description

Given a roman numeral, convert it to an integer.

Input is guaranteed to be within the range from 1 to 3999.

Tags: Math, String

思路

题意是罗马数字转整型数,范围从 1 到 3999,查看下百度百科的罗马数字介绍如下:

  • 相同的数字连写,所表示的数等于这些数字相加得到的数,如 Ⅲ=3;

  • 小的数字在大的数字的右边,所表示的数等于这些数字相加得到的数,如 Ⅷ=8、Ⅻ=12;

  • 小的数字(限于 Ⅰ、X 和 C)在大的数字的左边,所表示的数等于大数减小数得到的数,如 Ⅳ=4、Ⅸ=9。

那么我们可以利用 map 来完成罗马数字的 7 个数字符号:I、V、X、L、C、D、M 和整数的映射关系,然后根据上面的解释来模拟完成即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public int romanToInt(String s) {
Map<Character, Integer> map = new HashMap<>();
map.put('I', 1);
map.put('V', 5);
map.put('X', 10);
map.put('L', 50);
map.put('C', 100);
map.put('D', 500);
map.put('M', 1000);
int len = s.length();
int sum = map.get(s.charAt(len - 1));
for (int i = len - 2; i >= 0; --i) {
if (map.get(s.charAt(i)) < map.get(s.charAt(i + 1))) {
sum -= map.get(s.charAt(i));
} else {
sum += map.get(s.charAt(i));
}
}
return sum;
}
}

014 Longest Common Prefix

Description

Write a function to find the longest common prefix string amongst an array of strings.

Tags: String

思路

题意是让你从字符串数组中找出公共前缀,我的想法是找出最短的那个字符串的长度 minLen,然后在 0...minLen 的范围比较所有字符串,如果比较到有不同的字符,那么直接返回当前索引长度的字符串即可,否则最后返回最短的字符串即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public String longestCommonPrefix(String[] strs) {
int len = strs.length;
if (len == 0) return "";
int minLen = 0x7fffffff;
for (String str : strs) minLen = Math.min(minLen, str.length());
for (int j = 0; j < minLen; ++j)
for (int i = 1; i < len; ++i)
if (strs[0].charAt(j) != strs[i].charAt(j))
return strs[0].substring(0, j);
return strs[0].substring(0, minLen);
}
}

015 3Sum

Description

Given an array S of n integers, are there elements a, b, c in S such that a + b + c = 0? Find all unique triplets in the array which gives the sum of zero.

Note: The solution set must not contain duplicate triplets.

1
2
3
4
5
6
7
For example, given array S = [-1, 0, 1, 2, -1, -4],
A solution set is:
[
[-1, 0, 1],
[-1, -1, 2]
]

Tags: Array, Two Pointers

思路

题意是让你从数组中找出所有三个和为 0 的元素构成的非重复序列,这样的话我们可以把数组先做下排序,然后遍历这个排序数组,用两个指针分别指向当前元素的下一个和数组尾部,判断三者的和与 0 的大小来移动两个指针,其中细节操作就是优化和去重。

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
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
int len = nums.length;
if (len < 3) return list;
Arrays.sort(nums);
int max = nums[len - 1];
if (max < 0) return list;
for (int i = 0; i < len - 2; ) {
if (nums[i] > 0) break;
if (nums[i] + 2 * max < 0) {
while (nums[i] == nums[++i] && i < len - 2) ;
continue;
}
int left = i + 1, right = len - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
list.add(Arrays.asList(nums[i], nums[left], nums[right]));
while (nums[left] == nums[++left] && left < right) ;
while (nums[right] == nums[--right] && left < right) ;
} else if (sum < 0) ++left;
else --right;
}
while (nums[i] == nums[++i] && i < len - 2) ;
}
return list;
}
}

016 3Sum Closest

Description

Given an array S of n integers, find three integers in S such that the sum is closest to a given number, target. Return the sum of the three integers. You may assume that each input would have exactly one solution.

1
2
3
For example, given array S = {-1 2 1 -4}, and target = 1.
The sum that is closest to the target is 2. (-1 + 2 + 1 = 2).

Tags: Array, Two Pointers

思路

题意是让你从数组中找出最接近 target 的三个数的和,该题和 [3Sum][015] 的思路基本一样,先对数组进行排序,然后遍历这个排序数组,用两个指针分别指向当前元素的下一个和数组尾部,判断三者的和与 target 的差值来移动两个指针,并更新其结果,当然,如果三者的和直接与 target 值相同,那么返回 target 结果即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Solution {
public int threeSumClosest(int[] nums, int target) {
int delta = 0x7fffffff, res = 0;
Arrays.sort(nums);
int len = nums.length - 2;
for (int i = 0; i < len; i++) {
int left = i + 1, right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
int curDelta = Math.abs(sum - target);
if (curDelta == 0) return sum;
if (curDelta < delta) {
delta = curDelta;
res = sum;
}
if (sum > target) --right;
else ++left;
}
}
return res;
}
}

017 Letter Combinations of a Phone Number

Description

Given a digit string, return all possible letter combinations that the number could represent.

A mapping of digit to letters (just like on the telephone buttons) is given below.

img

1
2
Input:Digit string "23"
Output: ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].

Note:

Although the above answer is in lexicographical order, your answer could be in any order you want.

Tags: String, Backtracking

思路 0

题意是给你按键,让你组合出所有不同结果,首先想到的肯定是回溯了,对每个按键的所有情况进行回溯,回溯的终点就是结果字符串长度和按键长度相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
private static String[] map = new String[]{"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
public List<String> letterCombinations(String digits) {
if (digits.length() == 0) return Collections.emptyList();
List<String> list = new ArrayList<>();
helper(list, digits, "");
return list;
}
private void helper(List<String> list, String digits, String ans) {
if (ans.length() == digits.length()) {
list.add(ans);
return;
}
for (char c : map[digits.charAt(ans.length()) - '2'].toCharArray()) {
helper(list, digits, ans + c);
}
}
}

思路 1

还有一种思路就是利用队列,根据上一次队列中的值,该值拼接当前可选值来不断迭代其结果,具体代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
private static String[] map = new String[]{"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
public List<String> letterCombinations(String digits) {
if (digits.length() == 0) return Collections.emptyList();
LinkedList<String> list = new LinkedList<>();
list.add("");
char[] charArray = digits.toCharArray();
for (int i = 0; i < charArray.length; i++) {
char c = charArray[i];
while (list.getFirst().length() == i) {
String pop = list.removeFirst();
for (char v : map[c - '2'].toCharArray()) {
list.addLast(pop + v);
}
}
}
return list;
}
}

018 4Sum

Description

Given an array S of n integers, are there elements a, b, c, and d in S such that a + b + c + d = target? Find all unique quadruplets in the array which gives the sum of target.

Note: The solution set must not contain duplicate quadruplets.

1
2
3
4
5
6
7
8
For example, given array S = [1, 0, -1, 0, -2, 2], and target = 0.
A solution set is:
[
[-1, 0, 0, 1],
[-2, -1, 1, 2],
[-2, 0, 0, 2]
]

Tags: Array, Hash Table, Two Pointers

思路 0

题意是让你从数组中找出所有四个数的和为 target 的元素构成的非重复序列,该题和 [3Sum][015] 的思路基本一样,先对数组进行排序,然后遍历这个排序数组,因为这次是四个元素的和,所以外层需要两重循环,然后还是用两个指针分别指向当前元素的下一个和数组尾部,判断四者的和与 target 的大小来移动两个指针,其中细节操作还是优化和去重。

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
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> res = new ArrayList<>();
int len = nums.length;
if (len < 4) return res;
Arrays.sort(nums);
int max = nums[len - 1];
if (4 * max < target) return res;
for (int i = 0; i < len - 3;) {
if (nums[i] * 4 > target) break;
if (nums[i] + 3 * max < target) {
while (nums[i] == nums[++i] && i < len - 3) ;
continue;
}
for (int j = i + 1; j < len - 2;) {
int subSum = nums[i] + nums[j];
if (nums[i] + nums[j] * 3 > target) break;
if (subSum + 2 * max < target) {
while (nums[j] == nums[++j] && j < len - 2) ;
continue;
}
int left = j + 1, right = len - 1;
while (left < right) {
int sum = subSum + nums[left] + nums[right];
if (sum == target) {
res.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
while (nums[left] == nums[++left] && left < right);
while (nums[right] == nums[--right] && left < right);
} else if (sum < target) ++left;
else --right;
}
while (nums[j] == nums[++j] && j < len - 2) ;
}
while (nums[i] == nums[++i] && i < len - 3) ;
}
return res;
}
}

思路 1

从 [Two Sum][001]、[3Sum][015] 到现在的 4Sum,其实都是把高阶降为低阶,那么我们就可以写出 kSum 的函数来对其进行降阶处理,降到 2Sum 后那么我们就可以对其进行最后的判断了,代码如下所示,其也做了相应的优化和去重。

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
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums);
int len = nums.length;
if (len < 4) return Collections.emptyList();
int max = nums[len - 1];
if (4 * max < target) return Collections.emptyList();
return kSum(nums, 0, 4, target);
}
private List<List<Integer>> kSum(int[] nums, int start, int k, int target) {
List<List<Integer>> res = new ArrayList<>();
if (k == 2) {
int left = start, right = nums.length - 1;
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) {
List<Integer> twoSum = new LinkedList<>();
twoSum.add(nums[left]);
twoSum.add(nums[right]);
res.add(twoSum);
while (nums[left] == nums[++left] && left < right) ;
while (nums[right] == nums[--right] && left < right) ;
} else if (sum < target) ++left;
else --right;
}
} else {
int i = start, end = nums.length - (k - 1), max = nums[nums.length - 1];
while (i < end) {
if (nums[i] * k > target) return res;
if (nums[i] + (k - 1) * max < target) {
while (nums[i] == nums[++i] && i < end) ;
continue;
}
List<List<Integer>> temp = kSum(nums, i + 1, k - 1, target - nums[i]);
for (List<Integer> t : temp) {
t.add(0, nums[i]);
}
res.addAll(temp);
while (nums[i] == nums[++i] && i < end) ;
}
}
return res;
}
}

019 Remove Nth Node From End of List

Description

Given a linked list, remove the nth node from the end of list and return its head.

For example,

1
2
3
Given linked list: 1->2->3->4->5, and n = 2.
After removing the second node from the end, the linked list becomes 1->2->3->5.

Note:

Given n will always be valid.

Try to do this in one pass.

Tags: Linked List, Two Pointers

思路

题意是让你删除链表中的倒数第 n 个数,我的解法是利用双指针,这两个指针相差 n 个元素,当后面的指针扫到链表末尾的时候,自然它前面的那个指针所指向的下一个元素就是要删除的元素,即 pre.next = pre.next.next;,但是如果一开始后面的指针指向的为空,此时代表的意思就是要删除第一个元素,即 head = head.next;

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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode pre = head;
ListNode afterPreN = head;
while (n-- != 0) {
afterPreN = afterPreN.next;
}
if (afterPreN != null) {
while (afterPreN.next != null) {
pre = pre.next;
afterPreN = afterPreN.next;
}
pre.next = pre.next.next;
} else {
head = head.next;
}
return head;
}
}

020 Valid Parentheses

Description

Given a string containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.

The brackets must close in the correct order, "()" and "()[]{}" are all valid but "(]" and "([)]" are not.

Tags: Stack, String

思路

题意是判断括号匹配是否正确,很明显,我们可以用栈来解决这个问题,当出现左括号的时候入栈,当遇到右括号时,判断栈顶的左括号是否何其匹配,不匹配的话直接返回 false 即可,最终判断是否空栈即可,这里我们可以用数组模拟栈的操作使其操作更快,有个细节注意下 top = 1;,从而省去了之后判空的操作和 top - 1 导致数组越界的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public boolean isValid(String s) {
char[] stack = new char[s.length() + 1];
int top = 1;
for (char c : s.toCharArray()) {
if (c == '(' || c == '[' || c == '{') {
stack[top++] = c;
} else if (c == ')' && stack[--top] != '(') {
return false;
} else if (c == ']' && stack[--top] != '[') {
return false;
} else if (c == '}' && stack[--top] != '{') {
return false;
}
}
return top == 1;
}
}

021 Merge Two Sorted Lists

Description

Merge two sorted linked lists and return it as a new list. The new list should be made by splicing together the nodes of the first two lists.

Example:

1
2
Input: 1->2->4, 1->3->4
Output: 1->1->2->3->4->4

Tags: Linked List

思路

题意是用一个新链表来合并两个已排序的链表,那我们只需要从头开始比较已排序的两个链表,新链表指针每次指向值小的节点,依次比较下去,最后,当其中一个链表到达了末尾,我们只需要把新链表指针指向另一个没有到末尾的链表此时的指针即可。

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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode head = new ListNode(0);
ListNode temp = head;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
temp.next = l1;
l1 = l1.next;
} else {
temp.next = l2;
l2 = l2.next;
}
temp = temp.next;
}
temp.next = l1 != null ? l1 : l2;
return head.next;
}
}

022 Generate Parentheses

Description

Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.

For example, given n = 3, a solution set is:

1
2
3
4
5
6
7
[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]

Tags: String, Backtracking

思路 0

题意是给你 n 值,让你找到所有格式正确的圆括号匹配组,题目中已经给出了 n = 3 的所有结果。遇到这种问题,第一直觉就是用到递归或者堆栈,我们选取递归来解决,也就是 helper 函数的功能,从参数上来看肯定很好理解了,leftRest 代表还有几个左括号可以用,rightNeed 代表还需要几个右括号才能匹配,初始状态当然是 rightNeed = 0, leftRest = n,递归的终止状态就是 rightNeed == 0 && leftRest == 0,也就是左右括号都已匹配完毕,然后把 str 加入到链表中即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public List<String> generateParenthesis(int n) {
List<String> list = new ArrayList<>();
helper(list, "", 0, n);
return list;
}
private void helper(List<String> list, String str, int rightNeed, int leftRest) {
if (rightNeed == 0 && leftRest == 0) {
list.add(str);
return;
}
if (rightNeed > 0) helper(list, str + ")", rightNeed - 1, leftRest);
if (leftRest > 0) helper(list, str + "(", rightNeed + 1, leftRest - 1);
}
}

思路 1

另一种实现方式就是迭代的思想了,我们来找寻其规律如下所示:

1
2
3
4
5
6
7
8
f(0): “”
f(1): “(“f(0)”)”
f(2): "(“f(0)”)"f(1), “(“f(1)”)”
f(3): "(“f(0)”)"f(2), "(“f(1)”)"f(1), “(“f(2)”)”
...

可以递推出 f(n) = "(“f(0)”)"f(n-1) , "(“f(1)”)"f(n-2) "(“f(2)”)"f(n-3) … "(“f(i)”)“f(n-1-i) … “(f(n-1)”)”

根据如上递推式写出如下代码应该不难了吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public List<String> generateParenthesis(int n) {
HashMap<Integer, List<String>> hashMap = new HashMap<>();
hashMap.put(0, Collections.singletonList(""));
for (int i = 1; i <= n; i++) {
List<String> list = new ArrayList<>();
for (int j = 0; j < i; j++) {
for (String fj : hashMap.get(j)) {
for (String fi_j_1 : hashMap.get(i - j - 1)) {
list.add("(" + fj + ")" + fi_j_1);// calculate f(i)
}
}
}
hashMap.put(i, list);
}
return hashMap.get(n);
}
}

023 Merge k Sorted Lists

Description

Merge k sorted linked lists and return it as one sorted list. Analyze and describe its complexity.

Tags: Linked List, Divide and Conquer, Heap

思路 0

题意是合并多个已排序的链表,分析并描述其复杂度,我们可以用分治法来两两合并他们,假设 k 为总链表个数,N 为总元素个数,那么其时间复杂度为 O(Nlogk)

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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if (lists.length == 0) return null;
return helper(lists, 0, lists.length - 1);
}
private ListNode helper(ListNode[] lists, int left, int right) {
if (left >= right) return lists[left];
int mid = left + right >>> 1;
ListNode l0 = helper(lists, left, mid);
ListNode l1 = helper(lists, mid + 1, right);
return merge2Lists(l0, l1);
}
private ListNode merge2Lists(ListNode l0, ListNode l1) {
ListNode node = new ListNode(0), tmp = node;
while (l0 != null && l1 != null) {
if (l0.val <= l1.val) {
tmp.next = new ListNode(l0.val);
l0 = l0.next;
} else {
tmp.next = new ListNode(l1.val);
l1 = l1.next;
}
tmp = tmp.next;
}
tmp.next = l0 != null ? l0 : l1;
return node.next;
}
}

思路 1

还有另一种思路是利用优先队列,该数据结构用到的是堆排序,所以对总链表个数为 k 的复杂度为 logk,总元素为个数为 N 的话,其时间复杂度也为 O(Nlogk)

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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if (lists.length == 0) return null;
PriorityQueue<ListNode> queue = new PriorityQueue<>(lists.length, new Comparator<ListNode>() {
@Override
public int compare(ListNode o1, ListNode o2) {
if (o1.val < o2.val) return -1;
else if (o1.val == o2.val) return 0;
else return 1;
}
});
ListNode node = new ListNode(0), tmp = node;
for (ListNode l : lists) {
if (l != null) queue.add(l);
}
while (!queue.isEmpty()) {
tmp.next = queue.poll();
tmp = tmp.next;
if (tmp.next != null) queue.add(tmp.next);
}
return node.next;
}
}

024 Swap Nodes in Pairs

Description

Given a linked list, swap every two adjacent nodes and return its head.

For example,
Given 1->2->3->4, you should return the list as 2->1->4->3.

Your algorithm should use only constant space. You may not modify the values in the list, only nodes itself can be changed.

Tags: Linked List

思路 0

题意是让你交换链表中相邻的两个节点,最终返回交换后链表的头,限定你空间复杂度为 O(1)。我们可以用递归来算出子集合的结果,递归的终点就是指针指到链表末少于两个元素时,如果不是终点,那么我们就对其两节点进行交换,这里我们需要一个临时节点来作为交换桥梁,就不多说了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null) return head;
ListNode node = head.next;
head.next = swapPairs(node.next);
node.next = head;
return node;
}
}

思路 1

另一种实现方式就是用循环来实现了,两两交换节点,也需要一个临时节点来作为交换桥梁,直到当前指针指到链表末少于两个元素时停止,代码很简单,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode preHead = new ListNode(0), cur = preHead;
preHead.next = head;
while (cur.next != null && cur.next.next != null) {
ListNode temp = cur.next.next;
cur.next.next = temp.next;
temp.next = cur.next;
cur.next = temp;
cur = cur.next.next;
}
return preHead.next;
}
}

025 Reverse Nodes in k-Group

Description

Given a linked list, reverse the nodes of a linked list k at a time and return its modified list.

k is a positive integer and is less than or equal to the length of the linked list. If the number of nodes is not a multiple of k then left-out nodes in the end should remain as it is.

You may not alter the values in the nodes, only nodes itself may be changed.

Only constant memory is allowed.

For example,

Given this linked list: 1->2->3->4->5

For k = 2, you should return: 2->1->4->3->5

For k = 3, you should return: 3->2->1->4->5

Tags: Linked List

思路

题意是让你以 k 个元素为一组来翻转链表,最后不足 k 个的话不需要翻转,最传统的思路就是每遇到 k 个元素,对其 k 个元素进行翻转,而难点就是在此,下面介绍其原理,我们以例子中的 k = 3 为例,其 prenext 如下所示。

1
2
3
0->1->2->3->4->5
| |
pre next

我们要做的就是把 prenext 之间的元素逆序,思想是依次从第二个元素到第 k 个元素,依次把它插入到 pre 后面,过程如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
head move
| |
0->1->2->3->4->5
| |
pre next
head move
| |
0->2->1->3->4->5
| |
pre next
head move
| |
0->3->2->1->4->5
| |
pre next

好了,根据原理,那写出代码就不难了。

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
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null || k == 1) return head;
ListNode node = new ListNode(0), pre = node;
node.next = head;
for (int i = 1; head != null; ++i) {
if (i % k == 0) {
pre = reverse(pre, head.next);
head = pre.next;
} else {
head = head.next;
}
}
return node.next;
}
private ListNode reverse(ListNode pre, ListNode next) {
ListNode head = pre.next;
ListNode move = head.next;
while (move != next) {
head.next = move.next;
move.next = pre.next;
pre.next = move;
move = head.next;
}
return head;
}
}

026 Remove Duplicates from Sorted Array

Description

Given a sorted array, remove the duplicates in-place such that each element appear only once and return the new length.

Do not allocate extra space for another array, you must do this by modifying the input array in-place with O(1) extra memory.

Example:

1
2
3
4
Given nums = [1,1,2],
Your function should return length = 2, with the first two elements of nums being 1 and 2 respectively.
It doesn't matter what you leave beyond the new length.

Tags: Array, Two Pointers

思路

题意是让你从一个有序的数组中移除重复的元素,并返回之后数组的长度。我的思路是判断长度小于等于 1 的话直接返回原长度即可,否则的话遍历一遍数组,用一个 tail 变量指向尾部,如果后面的元素和前面的元素不同,就让 tail 变量加一,最后返回 tail 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public int removeDuplicates(int[] nums) {
int len = nums.length;
if (len <= 1) return len;
int tail = 1;
for (int i = 1; i < len; ++i) {
if (nums[i - 1] != nums[i]) {
nums[tail++] = nums[i];
}
}
return tail;
}
}

027 Remove Element

Description

Given an array and a value, remove all instances of that value in-place and return the new length.

Do not allocate extra space for another array, you must do this by modifying the input array in-place with O(1) extra memory.

The order of elements can be changed. It doesn’t matter what you leave beyond the new length.

Example:

1
2
3
Given nums = [3,2,2,3], val = 3,
Your function should return length = 2, with the first two elements of nums being 2.

Tags: Array, Two Pointers

思路

题意是移除数组中值等于 val 的元素,并返回之后数组的长度,并且题目中指定空间复杂度为 O(1),我的思路是用 tail 标记尾部,遍历该数组时当索引元素不等于 val 时,tail 加一,尾部指向当前元素,最后返回 tail 即可。

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public int removeElement(int[] nums, int val) {
int tail = 0;
for (int i = 0, len = nums.length; i < len; ++i) {
if (nums[i] != val) {
nums[tail++] = nums[i];
}
}
return tail;
}
}

028 Implement strStr()

Description

Implement strStr().

Return the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.

Example 1:

1
2
Input: haystack = "hello", needle = "ll"
Output: 2

Example 2:

1
2
Input: haystack = "aaaaa", needle = "bba"
Output: -1

Tags:** Two Pointers, String

思路

题意是从主串中找到子串的索引,如果找不到则返回-1,当子串长度大于主串,直接返回-1,然后我们只需要遍历比较即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public int strStr(String haystack, String needle) {
int l1 = haystack.length(), l2 = needle.length();
if (l1 < l2) return -1;
for (int i = 0; ; i++) {
if (i + l2 > l1) return -1;
for (int j = 0; ; j++) {
if (j == l2) return i;
if (haystack.charAt(i + j) != needle.charAt(j)) break;
}
}
}
}

029 Divide Two Integers

Description

Divide two integers without using multiplication, division and mod operator.

If it is overflow, return MAX_INT.

Tags: Math, Binary Search

思路

题意是让你算两个整型数相除后的结果,如果结果溢出就返回 MAX_INT,但不能使用乘、除、余的操作符,如果是用加减操作符的话肯定会超时哈,这样的话我们就只能想到位操作符了。

首先,我们分析下溢出的情况,也就是当被除数为 Integer.MIN_VALUE,除数为 -1 时会溢出,因为 |Integer.MIN_VALUE| = Integer.MAX_VALUE + 1

然后,我们把除数和被除数都转为 long 类型的正数去做下一步操作,我这里以 223 相除为例子,因为 22 >= 3,我们对 3 进行左移一位,也就是乘 2,结果为 6,比 22 小,我们继续对 6 左移一位结果为 12,还是比 22 小,我们继续对 12 左移一位为 24,比 22 大,这时我们可以分析出,22 肯定比 34 倍要大,4 是怎么来的?因为我们左移了两次,也就是 1 << 2 = 4,此时我们记下这个 4,然后让 22 - 3 * 4 = 10,因为 10 >= 3,对 103 进行同样的操作,我们可以得到 2,此时加上上次的 4,和为 6,也就是 2236 倍要大,此时还剩余 10 - 6 = 4,因为 4 >= 3,所以对 43 进行同样的操作,我们发现并不能对 3 进行左移了,因为 4 >= 3,所以 1 倍还是有的,所以加上最后的 1,结果为 6 + 1 = 7,也就是 22 整除 3 结果为 7

最终,我们对结果赋予符号位即可,根据以上思路来书写如下代码应该不是难事了吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public int divide(int dividend, int divisor) {
if (dividend == Integer.MIN_VALUE && divisor == -1) {
return Integer.MAX_VALUE;
}
long dvd = Math.abs((long) dividend);
long dvr = Math.abs((long) divisor);
int res = 0;
while (dvd >= dvr) {
long temp = dvr, multiple = 1;
while (dvd >= temp << 1) {
temp <<= 1;
multiple <<= 1;
}
dvd -= temp;
res += multiple;
}
return (dividend < 0) ^ (divisor < 0) ? -res : res;
}
}

033 Search in Rotated Sorted Array

Description

Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforehand.

(i.e., 0 1 2 4 5 6 7 might become 4 5 6 7 0 1 2).

You are given a target value to search. If found in the array return its index, otherwise return -1.

You may assume no duplicate exists in the array.

Tags: Arrays, Binary Search

思路

题意是让你从一个旋转过后的递增序列中寻找给定值,找到返回索引,找不到返回-1,我们在下面这组数据中寻找规律。

1
2
3
4
5
6
1 2 4 5 6 7 0
2 4 5 6 7 0 1
4 5 6 7 0 1 2
5 6 7 0 1 2 4
6 7 0 1 2 4 5
7 0 1 2 4 5 6

由于是旋转一次,所以肯定有一半及以上的序列仍然是具有递增性质的,我们观察到如果中间的数比左面的数大的话,那么左半部分序列是递增的,否则右半部分就是递增的,那么我们就可以确定给定值是否在递增序列中,从而决定取舍哪半边。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int search(int[] nums, int target) {
int l = 0, r = nums.length - 1, mid;
while (l <= r) {
mid = l + r >>> 1;
if (nums[mid] == target) return mid;
else if (nums[mid] >= nums[l]) {
if (nums[l] <= target && target < nums[mid]) r = mid - 1;
else l = mid + 1;
} else {
if (nums[mid] < target && target <= nums[r]) l = mid + 1;
else r = mid - 1;
}
}
return -1;
}
}

035 Search Insert Position

Description

Given a sorted array and a target value, return the index if the target is found. If not, return the index where it would be if it were inserted in order.

You may assume no duplicates in the array.

Example 1:

1
2
Input: [1,3,5,6], 5
Output: 2

Example 2:

1
2
Input: [1,3,5,6], 2
Output: 1

Example 3:

1
2
Input: [1,3,5,6], 7
Output: 4

Example 1:

1
2
Input: [1,3,5,6], 0
Output: 0

Tags: Array, Binary Search

思路

题意是让你从一个没有重复元素的已排序数组中找到插入位置的索引。因为数组已排序,所以我们可以想到二分查找法,因为查找到的条件是找到第一个等于或者大于 target 的元素的位置,所以二分法略作变动即可。

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length - 1, mid = (right + left) >> 1;
while (left <= right) {
if (target <= nums[mid]) right = mid - 1;
else left = mid + 1;
mid = (right + left) >> 1;
}
return left;
}
}

038 Count and Say

Description

The count-and-say sequence is the sequence of integers with the first five terms as following:

1
2
3
4
5
1. 1
2. 11
3. 21
4. 1211
5. 111221

1 is read off as "one 1" or 11.

11 is read off as "two 1s" or 21.

21 is read off as "one 2, then one 1" or 1211.

Given an integer n, generate the nth term of the count-and-say sequence.

Note: Each term of the sequence of integers will be represented as a string.

Example 1:

1
2
Input: 1
Output: "1"

Example 2:

1
2
Input: 4
Output: "1211"

Tags: String

思路

题意是数和说,根据如下序列 1, 11, 21, 1211, 111221, ...,求第 n 个数,规则很简单,就是数和说,数就是数这个数数字有几个,说就是说这个数,所以 1 就是 1 个 1:11,11 就是有 2 个 1:2121 就是 1 个 2、1 个 1:1211,可想而知后面就是 111221,思路的话就是按这个逻辑模拟出来即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public String countAndSay(int n) {
String str = "1";
while (--n > 0) {
int times = 1;
StringBuilder sb = new StringBuilder();
char[] chars = str.toCharArray();
int len = chars.length;
for (int j = 1; j < len; j++) {
if (chars[j - 1] == chars[j]) {
times++;
} else {
sb.append(times).append(chars[j - 1]);
times = 1;
}
}
str = sb.append(times).append(chars[len - 1]).toString();
}
return str;
}
}

043 Multiply Strings

Description

Given two non-negative integers num1 and num2 represented as strings, return the product of num1 and num2.

Note:

  1. The length of both num1 and num2 is < 110.

  2. Both num1 and num2 contains only digits 0-9.

  3. Both num1 and num2 does not contain any leading zero.

  4. You must not use any built-in BigInteger library or convert the inputs to integer directly.

Tags: Math, String

思路

题意是让你计算两个非负字符串的乘积,我们模拟小学数学的方式来做,一位一位模拟计算,再各位累加。

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
class Solution {
public String multiply(String num1, String num2) {
if (num1.equals("0") || num2.equals("0")) return "0";
int l1 = num1.length(), l2 = num2.length(), l = l1 + l2;
char[] ans = new char[l];
char[] c1 = num1.toCharArray();
char[] c2 = num2.toCharArray();
for (int i = l1 - 1; i >= 0; --i) {
int c = c1[i] - '0';
for (int j = l2 - 1; j >= 0; --j) {
ans[i + j + 1] += c * (c2[j] - '0');
}
}
for (int i = l - 1; i > 0; ++i) {
if (ans[i] > 9) {
ans[i - 1] += ans[i] / 10;
ans[i] %= 10;
}
}
StringBuilder sb = new StringBuilder();
int i = 0;
for (; ; ++i) if (ans[i] != 0) break;
for (; i < ans.length; ++i) sb.append((char) (ans[i] + '0'));
return sb.toString();
}
}

044 Wildcard Matching

Description

Implement wildcard pattern matching with support for '?' and '*'.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'?' Matches any single character.
'*' Matches any sequence of characters (including the empty sequence).
The matching should cover the entire input string (not partial).
The function prototype should be:
bool isMatch(const char *s, const char *p)
Some examples:
isMatch("aa","a") → false
isMatch("aa","aa") → true
isMatch("aaa","aa") → false
isMatch("aa", "*") → true
isMatch("aa", "a*") → true
isMatch("ab", "?*") → true
isMatch("aab", "c*a*b") → false

Tags: String, Dynamic Programming, Backtracking, Greedy

思路 0

题意是让让你从判断 s 字符串是否通配符匹配于 p,这道题和[Regular Expression Matching][010]很是相似,区别在于 *,正则匹配的 * 不能单独存在,前面必须具有一个字符,其意义是表明前面的这个字符个数可以是任意个数,包括 0 个;而通配符的 * 是可以随意出现的,跟前面字符没有任何关系,其作用是可以表示任意字符串。在此我们可以利用 贪心算法 来解决这个问题,需要两个额外指针 pmatch 来分别记录最后一个 * 的位置和 * 匹配到 s 字符串的位置,其贪心体现在如果遇到 *,那就尽可能取匹配后方的内容,如果匹配失败,那就回到上一个遇到 * 的位置来继续贪心。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public boolean isMatch(String s, String p) {
if (p.length() == 0) return s.length() == 0;
int si = 0, pi = 0, match = 0, star = -1;
int sl = s.length(), pl = p.length();
char[] sc = s.toCharArray(), pc = p.toCharArray();
while (si < sl) {
if (pi < pl && (pc[pi] == sc[si] || pc[pi] == '?')) {
si++;
pi++;
} else if (pi < pl && pc[pi] == '*') {
star = pi++;
match = si;
} else if (star != -1) {
si = ++match;
pi = star + 1;
} else return false;
}
while (pi < pl && pc[pi] == '*') pi++;
return pi == pl;
}
}

思路 1

另一种思路就是动态规划了,我们定义 dp[i][j] 的真假来表示 s[0..i) 是否匹配 p[0..j),其状态转移方程如下所示:

  • 如果 p[j - 1] != '*'P[i][j] = P[i - 1][j - 1] && (s[i - 1] == p[j - 1] || p[j - 1] == '?');

  • 如果 p[j - 1] == '*'P[i][j] = P[i][j - 1] || P[i - 1][j]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public boolean isMatch(String s, String p) {
if (p.length() == 0) return s.length() == 0;
int sl = s.length(), pl = p.length();
boolean[][] dp = new boolean[sl + 1][pl + 1];
char[] sc = s.toCharArray(), pc = p.toCharArray();
dp[0][0] = true;
for (int i = 1; i <= pl; ++i) {
if (pc[i - 1] == '*') dp[0][i] = dp[0][i - 1];
}
for (int i = 1; i <= sl; ++i) {
for (int j = 1; j <= pl; ++j) {
if (pc[j - 1] != '*') {
dp[i][j] = dp[i - 1][j - 1] && (sc[i - 1] == pc[j - 1] || pc[j - 1] == '?');
} else {
dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
}
}
}
return dp[sl][pl];
}
}

049 Group Anagrams

Description

Given an array of strings, group anagrams together.

For example, given: ["eat", "tea", "tan", "ate", "nat", "bat"],

Return:

1
2
3
4
5
[
["ate", "eat","tea"],
["nat","tan"],
["bat"]
]

Note: All inputs will be in lower-case.

Tags: Hash Table, String

思路

题意是给你一组字符串,让你把其中同位异构字符串分组,同位异构字符串就是组成字符串的字符都相同,但是字符放的位置可以不同。既然要分组,那么关键就是如何确定他们是同位异构字符串呢,想到的自然就是对其排序,排序之后他们就都是同一个字符串了,就可以归为一类了,代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
if (strs == null || strs.length == 0) return Collections.emptyList();
List<List<String>> list = new ArrayList<>();
Map<String, Integer> hash = new HashMap<>();
int i = 0;
for (String str : strs) {
char[] c = str.toCharArray();
Arrays.sort(c);
String sortStr = String.valueOf(c);
if (!hash.containsKey(sortStr)) {
hash.put(sortStr, i++);
List<String> sub = new ArrayList<>();
sub.add(str);
list.add(sub);
} else {
list.get(hash.get(sortStr)).add(str);
}
}
return list;
}
}

050 Pow(x, n)

Description

Implement pow(x, n).

Example 1:

1
2
Input: 2.00000, 10
Output: 1024.00000

Example 2:

1
2
Input: 2.10000, 3
Output: 9.26100

Tags: Math, Binary Search

思路

题意是让你计算 x^n,如果直接计算肯定会超时,那么我们可以想到可以使用二分法来降低时间复杂度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public double myPow(double x, int n) {
if (n < 0) return helper(1 / x, -n);
return helper(x, n);
}
private double helper(double x, int n) {
if (n == 0) return 1;
if (n == 1) return x;
double d = helper(x, n >>> 1);
if (n % 2 == 0) return d * d;
return d * d * x;
}
}

053 Maximum Subarray

Description

Find the contiguous subarray within an array (containing at least one number) which has the largest sum.

For example, given the array [-2,1,-3,4,-1,2,1,-5,4],

the contiguous subarray [4,-1,2,1] has the largest sum = 6.

More practice:

If you have figured out the O(n) solution, try coding another solution using the divide and conquer approach, which is more subtle.

Tags: Array, Divide and Conquer, Dynamic Programming

思路 0

题意是求数组中子数组的最大和,这种最优问题一般第一时间想到的就是动态规划,我们可以这样想,当部分序列和大于零的话就一直加下一个元素即可,并和当前最大值进行比较,如果出现部分序列小于零的情况,那肯定就是从当前元素算起。其转移方程就是 dp[i] = nums[i] + (dp[i - 1] > 0 ? dp[i - 1] : 0);,由于我们不需要保留 dp 状态,故可以优化空间复杂度为 1,即 dp = nums[i] + (dp > 0 ? dp : 0);

1
2
3
4
5
6
7
8
9
10
class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length, dp = nums[0], max = dp;
for (int i = 1; i < len; ++i) {
dp = nums[i] + (dp > 0 ? dp : 0);
if (dp > max) max = dp;
}
return max;
}
}

思路 1

题目也给了我们另一种思路,就是分治,所谓分治就是把问题分割成更小的,最后再合并即可,我们把 nums 一分为二先,那么就有两种情况,一种最大序列包括中间的值,一种就是不包括,也就是在左边或者右边;当最大序列在中间的时候那我们就把它两侧的最大和算出即可;当在两侧的话就继续分治即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public int maxSubArray(int[] nums) {
return helper(nums, 0, nums.length - 1);
}
private int helper(int[] nums, int left, int right) {
if (left >= right) return nums[left];
int mid = (left + right) >> 1;
int leftAns = helper(nums, left, mid);
int rightAns = helper(nums, mid + 1, right);
int leftMax = nums[mid], rightMax = nums[mid + 1];
int temp = 0;
for (int i = mid; i >= left; --i) {
temp += nums[i];
if (temp > leftMax) leftMax = temp;
}
temp = 0;
for (int i = mid + 1; i <= right; ++i) {
temp += nums[i];
if (temp > rightMax) rightMax = temp;
}
return Math.max(Math.max(leftAns, rightAns), leftMax + rightMax);
}
}

056 Merge Intervals

Description

Given a collection of intervals, merge all overlapping intervals.

For example,

Given [1,3],[2,6],[8,10],[15,18],

return [1,6],[8,10],[15,18].

Tags: Array, Sort

思路

题意是给你一组区间,让你把区间合并成没有交集的一组区间。我们可以把区间按 start 进行排序,然后遍历排序后的区间,如果当前的 start 小于前者的 end,那么说明这两个存在交集,我们取两者中较大的 end 即可;否则的话直接插入到结果序列中即可。

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
/**
* Definition for an interval.
* public class Interval {
* int start;
* int end;
* Interval() { start = 0; end = 0; }
* Interval(int s, int e) { start = s; end = e; }
* }
*/
class Solution {
public List<Interval> merge(List<Interval> intervals) {
if (intervals == null || intervals.size() <= 1) return intervals;
intervals.sort(new Comparator<Interval>() {
@Override
public int compare(Interval o1, Interval o2) {
if (o1.start < o2.start) return -1;
if (o1.start > o2.start) return 1;
return 0;
}
});
List<Interval> ans = new ArrayList<>();
int start = intervals.get(0).start;
int end = intervals.get(0).end;
for (Interval interval : intervals) {
if (interval.start <= end) {
end = Math.max(end, interval.end);
} else {
ans.add(new Interval(start, end));
start = interval.start;
end = interval.end;
}
}
ans.add(new Interval(start, end));
return ans;
}
}

057 Insert Interval

Description

Given a set of non-overlapping intervals, insert a new interval into the intervals (merge if necessary).

You may assume that the intervals were initially sorted according to their start times.

Example 1:

Given intervals [1,3],[6,9], insert and merge [2,5] in as [1,5],[6,9].

Example 2:

Given [1,2],[3,5],[6,7],[8,10],[12,16], insert and merge [4,9] in as [1,2],[3,10],[12,16].

This is because the new interval [4,9] overlaps with [3,5],[6,7],[8,10].

Tags: Array, Sort

思路

题意是给你一组有序区间,和一个待插入区间,让你待插入区间插入到前面的区间中,我们分三步走:

  1. 首先把有序区间中小于待插入区间的部分加入到结果中;

  2. 其次是插入待插入区间,如果有交集的话取两者交集的端点值;

  3. 最后把有序区间中大于待插入区间的部分加入到结果中;

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
/**
* Definition for an interval.
* public class Interval {
* int start;
* int end;
* Interval() { start = 0; end = 0; }
* Interval(int s, int e) { start = s; end = e; }
* }
*/
class Solution {
public List<Interval> insert(List<Interval> intervals, Interval newInterval) {
if (intervals.isEmpty()) return Collections.singletonList(newInterval);
List<Interval> ans = new ArrayList<>();
int i = 0, len = intervals.size();
for (; i < len; ++i) {
Interval interval = intervals.get(i);
if (interval.end < newInterval.start) ans.add(interval);
else break;
}
for (; i < len; ++i) {
Interval interval = intervals.get(i);
if (interval.start <= newInterval.end) {
newInterval.start = Math.min(newInterval.start, interval.start);
newInterval.end = Math.max(newInterval.end, interval.end);
} else break;
}
ans.add(newInterval);
for (; i < len; ++i) {
ans.add(intervals.get(i));
}
return ans;
}
}

058 Length of Last Word

Description

Given a string s consists of upper/lower-case alphabets and empty space characters ' ', return the length of last word in the string.

If the last word does not exist, return 0.

Note: A word is defined as a character sequence consists of non-space characters only.

Example:

1
2
Input: "Hello World"
Output: 5

Tags: String

思路

题意是让你从一个只包含大小字母和空格字符的字符串中得到最后一个单词的长度,很简单,我们倒序遍历,先得到最后一个非空格字符的索引,然后再得到它前面的空格字符索引,两者相减即可。当然,我们使用 API 来完成这件事更加方便,只需一行代码 return s.trim().length() - s.trim().lastIndexOf(" ") - 1;,但我相信作者出这道题的目的肯定不是考你 API 的使用,所以我们还是用自己的思路来实现。

1
2
3
4
5
6
7
8
9
class Solution {
public int lengthOfLastWord(String s) {
int p = s.length() - 1;
while (p >= 0 && s.charAt(p) == ' ') p--;
int end = p;
while (p >= 0 && s.charAt(p) != ' ') p--;
return end - p;
}
}

066 Plus One

Description

Given a non-negative integer represented as a non-empty array of digits, plus one to the integer.

You may assume the integer do not contain any leading zero, except the number 0 itself.

The digits are stored such that the most significant digit is at the head of the list.

Tags: Array, Math

思路

题意是给你一个数字数组,高位在前,并且首位不为 0 除非这个数组就是 [0],让你给该数组低位加一求其结果,那么我们就模拟小学数学那样进位去算即可,如果一直进位到首位,这种情况也就是都是由 9 组成的数组,此时我们只要 new 出一个多一个长度的数组即可,并把第 0 个元素赋 1 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public int[] plusOne(int[] digits) {
int p = digits.length - 1;
if (digits[p] < 9) {
digits[p] = ++digits[p];
} else {
do {
digits[p--] = 0;
} while (p >= 0 && digits[p] == 9);
if (digits[0] != 0) {
++digits[p];
} else {
digits = new int[digits.length + 1];
digits[0] = 1;
}
}
return digits;
}
}

067 Add Binary

Description

Given two binary strings, return their sum (also a binary string).

For example,

a = "11"

b = "1"

Return "100".

Tags: Math, String

思路

题意是给你两个二进制串,求其和的二进制串。我们就按照小学算数那么来做,用 carry 表示进位,从后往前算,依次往前,每算出一位就插入到最前面即可,直到把两个二进制串都遍历完即可。

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
class Solution {
public String addBinary(String a, String b) {
StringBuilder sb = new StringBuilder();
int carry = 0, p1 = a.length() - 1, p2 = b.length() - 1;
while (p1 >= 0 && p2 >= 0) {
carry += p1 >= 0 ? a.charAt(p1--) - '0' : 0;
carry += p2 >= 0 ? b.charAt(p2--) - '0' : 0;
sb.insert(0, (char) (carry % 2 + '0'));
carry >>= 1;
}
while (p1 >= 0) {
carry += p1 >= 0 ? a.charAt(p1--) - '0' : 0;
sb.insert(0, (char) (carry % 2 + '0'));
carry >>= 1;
}
while (p2 >= 0) {
carry += p2 >= 0 ? b.charAt(p2--) - '0' : 0;
sb.insert(0, (char) (carry % 2 + '0'));
carry >>= 1;
}
if (carry == 1) {
sb.insert(0, '1');
}
return sb.toString();
}
}

068 Text Justification

Description

Given an array of words and a length L, format the text such that each line has exactly L characters and is fully (left and right) justified.

You should pack your words in a greedy approach; that is, pack as many words as you can in each line. Pad extra spaces ' ' when necessary so that each line has exactly L characters.

Extra spaces between words should be distributed as evenly as possible. If the number of spaces on a line do not divide evenly between words, the empty slots on the left will be assigned more spaces than the slots on the right.

For the last line of text, it should be left justified and no extra space is inserted between words.

For example,

words: ["This", "is", "an", "example", "of", "text", "justification."]

L: 16.

Return the formatted lines as:

1
2
3
4
5
[
"This is an",
"example of text",
"justification. "
]

Note: Each word is guaranteed not to exceed L in length.

Corner Cases:

  • A line other than the last line might contain only one word. What should you do in this case?

    In this case, that line should be left-justified.

Tags: String

思路

题意是给你一组单词和最大行宽,让你对齐他们,对齐的规则就是尽可能一行可以放下足够多的单词,如果最后有多余的空格,那就把空格均匀地插入到单词之间,如果不能平分的话,那就从左开始依次多插一个空格,最后一行单词之间就正常地一个空格即可,如果凑不到最大行宽,那就在末尾补充空格即可,描述地比较差,不懂的话其实看看 demo 也就懂了哈。题还是比较坑的,毕竟踩的比赞的人多,我也是靠模拟老老实实做出来的,求出可以最多插入空格数,然后用它除以可以插入的槽数获取每个单词之间的空格,它两取余的话就是最面需要多插入一个空格的个数,最后一行的话就单独处理即可。

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
class Solution {
public List<String> fullJustify(String[] words, int maxWidth) {
int len = words.length;
List<String> ans = new ArrayList<>();
StringBuilder spaces = new StringBuilder();
for (int i = 0; i < maxWidth; ++i) {
spaces.append(" ");
}
int curLen = -1, start = 0;
for (int i = 0; i < len; ++i) {
if (curLen + words[i].length() + 1 <= maxWidth) {
curLen += words[i].length() + 1;
} else {
StringBuilder sub = new StringBuilder(words[start]);
int rest = maxWidth - curLen;
int l = i - start - 1;
if (l <= 0) {
sub.append(spaces.substring(0, rest));
} else {
int m = rest / l + 1;
int mod = rest % l;
for (int j = start + 1; j < i; ++j) {
if (mod-- > 0) {
sub.append(spaces.substring(0, m + 1)).append(words[j]);
} else {
sub.append(spaces.substring(0, m)).append(words[j]);
}
}
}
ans.add(sub.toString());
start = i;
curLen = words[i].length();
}
}
StringBuilder sub = new StringBuilder(words[start]);
for (int i = start + 1; i < len; ++i) {
sub.append(" ").append(words[i]);
}
ans.add(sub + spaces.substring(0, maxWidth - sub.length()));
return ans;
}
}

069 Sqrt(x)

Description

Implement int sqrt(int x).

Compute and return the square root of x.

x is guaranteed to be a non-negative integer.

Example 1:

1
2
Input: 4
Output: 2

Example 2:

1
2
3
Input: 8
Output: 2
Explanation: The square root of 8 is 2.82842..., and since we want to return an integer, the decimal part will be truncated.

Tags: Binary Search, Math

思路

题意是求平方根,参考 牛顿迭代法求平方根,然后再参考维基百科的 Integer square root 即可。

1
2
3
4
5
6
7
8
9
class Solution {
public int mySqrt(int x) {
long n = x;
while (n * n > x) {
n = (n + x / n) >> 1;
}
return (int) n;
}
}

070 Climbing Stairs

Description

You are climbing a stair case. It takes n steps to reach to the top.

Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?

Note: Given n will be a positive integer.

Example 1:

1
2
3
4
5
6
Input: 2
Output: 2
Explanation: There are two ways to climb to the top.
1. 1 step + 1 step
2. 2 steps

Example 2:

1
2
3
4
5
6
7
Input: 3
Output: 3
Explanation: There are three ways to climb to the top.
1. 1 step + 1 step + 1 step
2. 1 step + 2 steps
3. 2 steps + 1 step

Tags: Dynamic Programming

思路

题意是爬楼梯,每次你只能爬一步或者两步,问到顶层共有多少种方案。我们假设到顶层共有 f(n) 种,那么 f(n) = f(n - 1) + f(n - 2) 肯定是成立的,意思就是我们迈向顶层的最后一步是在倒数第一级台阶或者在倒数第二级台阶。算法我对空间复杂度进行了优化,因为在迭代过程中只需要两个变量即可。

1
2
3
4
5
6
7
8
9
10
class Solution {
public int climbStairs(int n) {
int a = 1, b = 1;
while (--n > 0) {
b += a;
a = b - a;
}
return b;
}
}

083Remove Duplicates from Sorted List

Description

Given a sorted linked list, delete all duplicates such that each element appear only once.

For example,

Given 1->1->2, return 1->2.

Given 1->1->2->3->3, return 1->2->3.

Tags: Linked List

思路

题意是删除链表中重复的元素,很简单,我们只需要遍历一遍链表,遇到链表中相邻元素相同时,把当前指针指向下下个元素即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null) return head;
ListNode curr = head;
while (curr.next != null) {
if (curr.next.val == curr.val) {
curr.next = curr.next.next;
} else {
curr = curr.next;
}
}
return head;
}
}

088 Merge Sorted Array

Description

Given two sorted integer arrays nums1 and nums2, merge nums2 into nums1 as one sorted array.

Note:

You may assume that nums1 has enough space (size that is greater or equal to m + n) to hold additional elements from nums2. The number of elements initialized in nums1 and nums2 are m and n respectively.

Tags: Array, Two Pointers

思路

题意是给两个已排序的数组 nums1nums2,合并 nums2nums1 中,两数组元素个数分别为 mn,而且 nums1 数组的长度足够容纳 m + n 个元素,如果我们按顺序排下去,那肯定要开辟一个新数组来保存元素,如果我们选择逆序,这样利用 nums1 自身空间足矣,不会出现覆盖的情况,依次把大的元素插入到 nums1 的末尾,确保 nums2 中的元素全部插入到 nums1 即可。

1
2
3
4
5
6
7
8
9
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int p = m-- + n-- - 1;
while (m >= 0 && n >= 0)
nums1[p--] = nums1[m] > nums2[n] ? nums1[m--] : nums2[n--];
while (n >= 0)
nums1[p--] = nums2[n--];
}
}

100 Same Tree

Description

Given two binary trees, write a function to check if they are the same or not.

Two binary trees are considered the same if they are structurally identical and the nodes have the same value.

Example 1:

1
2
3
4
5
6
7
Input: 1 1
/ \ / \
2 3 2 3
[1,2,3], [1,2,3]
Output: true

Example 2:

1
2
3
4
5
6
7
Input: 1 1
/ \
2 2
[1,2], [1,null,2]
Output: false

Example 3:

1
2
3
4
5
6
7
Input: 1 1
/ \ / \
2 1 1 2
[1,2,1], [1,1,2]
Output: false

Tags: Tree, Depth-first Search

思路

题意是比较两棵二叉树是否相同,那么我们就深搜比较各个节点即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
if (p == null && q == null) return true;
if (p == null || q == null) return false;
if (p.val == q.val) {
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
return false;
}
}

101 Symmetric Tree

Description

Given a binary tree, check whether it is a mirror of itself (ie, symmetric around its center).

For example, this binary tree [1,2,2,3,4,4,3] is symmetric:

1
2
3
4
5
1
/ \
2 2
/ \ / \
3 4 4 3

But the following [1,2,2,null,3,null,3] is not:

1
2
3
4
5
1
/ \
2 2
\ \
3 3

Note:

Bonus points if you could solve it both recursively and iteratively.

Tags: Tree, Depth-first Search, Breadth-first Search

思路 0

题意是判断一棵二叉树是否左右对称,首先想到的是深搜,比较根结点的左右两棵子树是否对称,如果左右子树的值相同,那么再分别对左子树的左节点和右子树的右节点,左子树的右节点和右子树的左节点做比较即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isSymmetric(TreeNode root) {
return root == null || helper(root.left, root.right);
}
public boolean helper(TreeNode left, TreeNode right) {
if (left == null || right == null) return left == right;
if (left.val != right.val) return false;
return helper(left.left, right.right) && helper(left.right, right.left);
}
}

思路 1

第二种思路就是宽搜了,宽搜肯定要用到队列,Java 中可用 LinkedList 替代,也是要做到左子树的左节点和右子树的右节点,左子树的右节点和右子树的左节点做比较即可。

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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isSymmetric(TreeNode root) {
if (root == null) return true;
LinkedList<TreeNode> q = new LinkedList<>();
q.add(root.left);
q.add(root.right);
TreeNode left, right;
while (q.size() > 1) {
left = q.pop();
right = q.pop();
if (left == null && right == null) continue;
if (left == null || right == null) return false;
if (left.val != right.val) return false;
q.add(left.left);
q.add(right.right);
q.add(left.right);
q.add(right.left);
}
return true;
}
}

104 Maximum Depth of Binary Tree

Description

Given a binary tree, find its maximum depth.

The maximum depth is the number of nodes along the longest path from the root node down to the farthest leaf node.

Tags: Tree, Depth-first Search

思路

题意是找到二叉树的最大深度,很明显,深搜即可,每深入一次节点加一即可,然后取左右子树的最大深度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
}

107 Binary Tree Level Order Traversal II

Description

Given a binary tree, return the bottom-up level order traversal of its nodes’ values. (ie, from left to right, level by level from leaf to root).

For example:

Given binary tree [3,9,20,null,null,15,7],

1
2
3
4
5
3
/ \
9 20
/ \
15 7

return its bottom-up level order traversal as:

1
2
3
4
5
[
[15,7],
[9,20],
[3]
]

Tags: Tree, Breadth-first Search

思路 0

题意是从下往上按层遍历二叉树,每一层是从左到右,按层遍历,很明显,宽搜第一时间符合,因为是从下往上,所以插入的时候每次插到链表头即可。

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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
if (root == null) return Collections.emptyList();
List<List<Integer>> list = new LinkedList<>();
LinkedList<TreeNode> q = new LinkedList<>();
q.add(root);
while(!q.isEmpty()) {
int size = q.size();
List<Integer> sub = new LinkedList();
for(int i = 0; i < size; ++i) {
TreeNode node = q.remove();
sub.add(node.val);
if (node.left != null) q.add(node.left);
if (node.right != null) q.add(node.right);
}
list.add(0, sub);
}
return list;
}
}

思路 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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> list = new LinkedList<>();
helper(list, root, 0);
return list;
}
private void helper(List<List<Integer>> list, TreeNode root, int level) {
if (root == null) return;
if (level >= list.size()) {
list.add(0, new LinkedList<>());
}
helper(list, root.left, level + 1);
helper(list, root.right, level + 1);
list.get(list.size() - level - 1).add(root.val);
}
}

108 Convert Sorted Array to Binary Search Tree

Description

Given an array where elements are sorted in ascending order, convert it to a height balanced BST.

For this problem, a height-balanced binary tree is defined as a binary tree in which the depth of the two subtrees of every node never differ by more than 1.

Example:

1
2
3
4
5
6
7
8
9
Given the sorted array: [-10,-3,0,5,9],
One possible answer is: [0,-3,9,-10,null,5], which represents the following height balanced BST:
0
/ \
-3 9
/ /
-10 5

Tags: Tree, Depth-first Search

思路

题意是把一个有序数组转化为一棵二叉搜索树,二叉搜索树具有以下性质:

  1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;

  2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;

  3. 任意节点的左、右子树也分别为二叉查找树;

  4. 没有键值相等的节点。

所以我们可以用递归来构建一棵二叉搜索树,每次把数组分为两半,把数组中间的值作为其父节点,然后把数组的左右两部分继续构造其左右子树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
if (nums == null || nums.length == 0) return null;
return helper(nums, 0, nums.length - 1);
}
private TreeNode helper(int[] nums, int left, int right) {
if (left > right) return null;
int mid = (left + right) >>> 1;
TreeNode node = new TreeNode(nums[mid]);
node.left = helper(nums, left, mid - 1);
node.right = helper(nums, mid + 1, right);
return node;
}
}

110 Balanced Binary Tree

Description

Given a binary tree, determine if it is height-balanced.

For this problem, a height-balanced binary tree is defined as a binary tree in which the depth of the two subtrees of every node never differ by more than 1.

Tags: Tree, Depth-first Search

思路

题意是判断一棵二叉树是否是高度平衡的,所谓二叉树高度平衡指的是二叉树的每个节点的两棵子树的高度差都不超过 1,那么我们只需计算左右子树的高度,判断其高度差是否不超过 1 即可,如果超过 1,就代表其不是高度平衡的,立即返回不是即可,我这里用返回 -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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isBalanced(TreeNode root) {
return helper(root) != -1;
}
private int helper(TreeNode node) {
if (node == null) return 0;
int l = helper(node.left);
if (l == -1) return -1;
int r = helper(node.right);
if (r == -1) return -1;
if (Math.abs(l - r) > 1) return -1;
return 1 + Math.max(l, r);
}
}

111 Minimum Depth of Binary Tree

Description

Given a binary tree, find its minimum depth.

The minimum depth is the number of nodes along the shortest path from the root node down to the nearest leaf node.

Tags: Tree, Depth-first Search, Breadth-first Search

思路 0

题意是查找二叉树的最小深度,也就是找到从根结点到叶子节点的最小深度,最容易想到的当然是深搜,如果节点的左右深度都不是 0 的话,说明该节点含有左右子树,所以它的最小高度就是 1 加上其左右子树高度较小者,否则如果左子树为空或者右子树为空或者两者都为空,那么就是 1 加上非空子树高度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int minDepth(TreeNode root) {
if (root == null) return 0;
int l = minDepth(root.left);
int r = minDepth(root.right);
if (l != 0 && r != 0) return 1 + Math.min(l, r);
return l + r + 1;
}
}

思路 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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int minDepth(TreeNode root) {
if (root == null) return 0;
LinkedList<TreeNode> q = new LinkedList<>();
q.add(root);
int ans = 1;
while (!q.isEmpty()) {
int size = q.size();
for (int i = 0; i < size; ++i) {
TreeNode node = q.remove();
if (node.left == null && node.right == null) {
return ans;
}
if (node.left != null) q.add(node.left);
if (node.right != null) q.add(node.right);
}
++ans;
}
return 520;
}
}

112 Path Sum

Description

Given a binary tree and a sum, determine if the tree has a root-to-leaf path such that adding up all the values along the path equals the given sum.

For example:

Given the below binary tree and sum = 22,

1
2
3
4
5
6
7
5
/ \
4 8
/ / \
11 13 4
/ \ \
7 2 1

return true, as there exist a root-to-leaf path 5->4->11->2 which sum is 22.

Tags: Tree, Depth-first Search

思路

题意是查找二叉树中是否存在从根结点到叶子的路径和为某一值,利用深搜在遇到叶子节点时判断是否满足即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean hasPathSum(TreeNode root, int sum) {
if (root == null) return false;
if (root.left == null && root.right == null) return sum == root.val;
return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val);
}
}

118 Pascal’s Triangle

Description

Given numRows, generate the first numRows of Pascal’s triangle.

For example, given numRows = 5,

Return

1
2
3
4
5
6
7
[
[1],
[1,1],
[1,2,1],
[1,3,3,1],
[1,4,6,4,1]
]

Tags: Array

思路

题意是给出行数,输出帕斯卡尔三角形,很简单的模拟,就不多说了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public List<List<Integer>> generate(int numRows) {
if (numRows == 0) return Collections.emptyList();
List<List<Integer>> list = new ArrayList<>();
for (int i = 0; i < numRows; ++i) {
List<Integer> sub = new ArrayList<>();
for (int j = 0; j <= i; ++j) {
if (j == 0 || j == i) {
sub.add(1);
} else {
List<Integer> upSub = list.get(i - 1);
sub.add(upSub.get(j - 1) + upSub.get(j));
}
}
list.add(sub);
}
return list;
}
}

119 Pascal’s Triangle II

Description

Given an index k, return the kth row of the Pascal’s triangle.

For example, given k = 3,

Return [1,3,3,1].

Note:

Could you optimize your algorithm to use only O(k) extra space?

Tags: Array

思路

题意是指定输出帕斯卡尔三角形的某一行,模拟即可,优化后的代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public List<Integer> getRow(int rowIndex) {
List<Integer> res = new ArrayList<>();
for (int i = 0; i <= rowIndex; ++i) {
res.add(1);
for (int j = i - 1; j > 0; --j) {
res.set(j, res.get(j - 1) + res.get(j));
}
}
return res;
}
}

121 Best Time to Buy and Sell Stock

Description

Say you have an array for which the ith element is the price of a given stock on day i.

If you were only permitted to complete at most one transaction (ie, buy one and sell one share of the stock), design an algorithm to find the maximum profit.

Example 1:

1
2
3
4
Input: [7, 1, 5, 3, 6, 4]
Output: 5
max. difference = 6-1 = 5 (not 7-1 = 6, as selling price needs to be larger than buying price)

Example 2:

1
2
3
4
Input: [7, 6, 4, 3, 1]
Output: 0
In this case, no transaction is done, i.e. max profit = 0.

Tags: Array, Dynamic Programmin

思路

题意是给出一个数组代表每天的股票金额,让你在最多买卖一次的情况下算出最大的收益额,最简单的就是模拟即可,每次记录当前值减去最小值的差值,与上一次的进行比较然后更新最大值即可。

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public int maxProfit(int[] prices) {
int max = 0, minPrice = Integer.MAX_VALUE;
for (int i = 0; i < prices.length; ++i) {
if (prices[i] < minPrice) minPrice = prices[i];
int delta = prices[i] - minPrice;
if (delta > max) max = delta;
}
return max;
}
}

122 Best Time to Buy and Sell Stock II

Description

Say you have an array for which the ith element is the price of a given stock on day i.

Design an algorithm to find the maximum profit. You may complete as many transactions as you like (ie, buy one and sell one share of the stock multiple times). However, you may not engage in multiple transactions at the same time (ie, you must sell the stock before you buy again).

Tags: Array, Greedy

思路

题意是给出一个数组代表每天的股票金额,在每天只能买或卖的情况下求出收益最高值,这…,这也太简单了吧,把所有相邻递增的值都加起来即可。

1
2
3
4
5
6
7
8
9
class Solution {
public int maxProfit(int[] prices) {
int max = 0;
for (int i = 1; i < prices.length; ++i) {
if (prices[i] > prices[i - 1]) max += prices[i] - prices[i - 1];
}
return max;
}
}

543 Diameter of Binary Tree

Description

Given a binary tree, you need to compute the length of the diameter of the tree. The diameter of a binary tree is the length of the longest path between any two nodes in a tree. This path may or may not pass through the root.

Example:

Given a binary tree

1
2
3
4
5
1
/ \
2 3
/ \
4 5

Return 3, which is the length of the path [4,2,1,3] or [5,2,1,3].

Note: The length of path between two nodes is represented by the number of edges between them.

Tags: Tree

思路

题意是让你算出二叉树中最远的两个节点的距离,分别计算左右子树的最大高度,然后不断迭代出其和的最大值就是最终结果。

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
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
int max = 0;
public int diameterOfBinaryTree(TreeNode root) {
helper(root);
return max;
}
private int helper(TreeNode root) {
if (root == null) return 0;
int l = helper(root.left);
int r = helper(root.right);
if (l + r > max) max = l + r;
return Math.max(l, r) + 1;
}
}

554 Brick Wall

Description

There is a brick wall in front of you. The wall is rectangular and has several rows of bricks. The bricks have the same height but different width. You want to draw a vertical line from the top to the bottom and cross the least bricks.

The brick wall is represented by a list of rows. Each row is a list of integers representing the width of each brick in this row from left to right.

If your line go through the edge of a brick, then the brick is not considered as crossed. You need to find out how to draw the line to cross the least bricks and return the number of crossed bricks.

You cannot draw a line just along one of the two vertical edges of the wall, in which case the line will obviously cross no bricks.

Example:

1
2
3
4
5
6
7
8
Input:
[[1,2,2,1],
[3,1,2],
[1,3,2],
[2,4],
[3,1,2],
[1,3,1,1]]
Output: 2

Explanation:
img

Note:

  1. The width sum of bricks in different rows are the same and won’t exceed INT_MAX.

  2. The number of bricks in each row is in range [1,10,000]. The height of wall is in range [1,10,000]. Total number of bricks of the wall won’t exceed 20,000.

Tags: Hash Table

思路

题意根据图示已经描述得很清楚了,就是在从底部到顶部,求最少交叉的数量,我们可以把每堵墙可以穿过的地方保存到哈希表中,每次遇到哈希表中的值加一,代表就是这条路不用交叉的数量,最终我们可以算出不用交叉的最大值,让总墙数减去其值就是最少交叉的数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public int leastBricks(List<List<Integer>> wall) {
Map<Integer, Integer> map = new HashMap<>();
int width = 0, max = 0;
for (List<Integer> sub : wall) {
int p = 0;
for (int i = 0, len = sub.size() - 1; i < len; ++i) {
p += sub.get(i);
Integer v = map.get(p);
map.put(p, (v == null ? 0 : v) + 1);
}
}
for (Integer integer : map.values()) {
if (integer > max) max = integer;
}
return wall.size() - max;
}
}

Java开发复习资料

  1. 前言
    1. 在被虐中成长,在面试中进步
      1. 1. 更加集中的学习
      2. 2. 更加系统的知识体系
      3. 3. 弥补不足,提升自我
      4. 4. 更加深入地了解知识背后原理
  2. 缘分与心态
  3. 基础备战
  • 第一章 操作系统
    1. 1.1 生产者与消费者
      1. 背景
      2. 实现方式
    2. 1.2 常见面试题
    3. 1.3进程和线程
      1. 1.1 线程
      2. 1.2 进程
      3. 1.3 进程和线程的关系:
      4. 1.4 进程与线程的区别
    4. 2. IPC几种通信方式(进程间的通信方式)
    5. 3. 死锁的必要条件和处理死锁。
  • 1.4 Window内存管理方式:页存储、段存储、段页存储。
    1. 1.4.1 分页存储管理
    2. 1.4.2 分段存储
    3. 1.4.3 分页和分段的主要区别
    4. 1.4.4 段页存储
  • 1.5 虚拟内存。
  • 1.6 操作系统如何进行分页调度
  • 1.7 磁盘调度算法:
  • 第二章 计算机网络
    1. 2.1 OSI(开放系统互联参考模型)标准模型
    2. 2.2 IP地址分类
    3. 2.3 ARP地址解析协议
    4. 2.4 交换机、路由器、网关
    5. 2.5 OSI 和TCP/IP的区别
    6. 2.6 TCP与UDP的区别
    7. 2.7 常见的路由选择协议,以及它们的区别
    8. 2.8 TCP的三次握手与四次挥手过程
    9. 2.9 TCP拥塞控制
    10. 2.10 HTTP
    11. 2.11 各种协议
  • 第三章 算法与数据结构
    1. 3.0 知识点汇总
    2. 3.1 排序算法
      1. 1.直接插入排序
      2. 2.希尔排序
      3. 3.冒泡排序
      4. 4.快速排序
      5. 5.直接选择排序
      6. 6.堆排序
      7. 7.归并排序
    3. 3.2 查找
      1. 1. 顺序查找
      2. 2. 二分查找
      3. 3. hash 算法
      4. 4. 二叉树搜索树
      5. 5. AVL查找树
      6. 6. B-树(B树)
      7. 7. B+树
      8. 8 . 红黑树
      9. 9. 哈夫曼树
    4. 3.3 字符串
      1. 排序
      2. 查找树
      3. 子字符串查找
    5. 3.4 图
    6. 3.5剑指offer
    7. 3.6 Leetcode
  • 第四章 JAVA基础
    1. 4.1 j2se基础
    2. 4.2 面向对象
    3. 4.3 集合
    4. 4.4 多线程
    5. 4.5 IO
    6. 4.6 设计模式
    7. 4.7 JVM
    8. JVM常用参数配置
  • 第五章 数据库
    1. 存储引擎的区别
    2. MyISAM和InnoDB的区别
      1. sql注入原理
    3. 数据库索引
    4. 数据库锁机制
    5. 数据库连接池原理
  • 第六章 框架和组件
    1. 6.1 spring
    2. 6.2 spring-MVC
    3. 6.3 Mybatis
  • 第七章 面试
    1. 7.1 语言基础
    2. 7.2 算法
    3. 7.3 项目
  • 前言

    在被虐中成长,在面试中进步

    指缝很宽,时间太瘦,悄悄从指缝间溜走。转眼之间三年的研究生生活即将结束,即使很是不舍,也终将要走向社会,开始另一段新的旅程。此刻,看着下一届师弟师妹开始忙碌地准备找实习,便开始追忆起过去一年找实习和工作的历程,那是一段艰辛而又成长颇多的经历。

    过去一年找实习和工作的经历还是那么清晰,每每想起,都彷如发生在昨日。面试官问的问题以及那些踩过的坑却都一一浮现眼前,久久不能忘怀。那无数日夜挑灯备战的艰辛,那焦虑不安等着面试电话的心情,那一次次被被拒后的失落和那收到一个个offer喜悦之情,那些我们经历的终将成为我们人生一笔宝贵的财富。而最重要的是,我们在面试过程中学会了成长,学会了改变。学习弥补那些我们不会的知识,加深理解那些我们浮于表面的原理,改变我们面试过程中出现的缺点和不足。那段时间,我们快速吸收了大量知识,又好像重新回到了当年的考研状态,但又感觉比考研还要艰辛,因为要准备的知识面太多、太广。亦如考研,朝看夕忘,每天沉浸在复习和备战中。书本和笔记一遍遍地翻看,算法一遍遍地刷,又一遍遍地准备那些自己不擅长的知识。闲暇时便又去收集各种招聘信息,看看招聘网站是否有新的找人消息,问问学长学姐还有没有内推的机会。就这样一步一步,我们在被虐中成长,在面试中进步。面试是一个非常好的学习机会,我们应当适当地利用好这次机会完成自己知识的一次飞跃。

    1. 更加集中的学习

    正如考研、期末考试一样,面试也是我们一次知识增长和爆发的时候。因为有了面试和找工作的压力,我们便能更加集中地将大把时间花在学习和复习上。将时间花在哪里,你的成就就在哪里,根据《刻意练习》中的刻意练习理论和《异类》中的10000小时定律,你在某个领域花的时间越多,你在该领域的成就越高。

    我们在面试过程中加强了知识复习强度,而面试就是对这次知识准备的反馈过程,通过面试来找出自己的不足和了解自己知识的掌握程度。遇到不会的知识,我们便开始上网或翻阅书本,查缺补漏。通过反复的准备、面试、纠正的过程使得我们掌握了大量的面试技巧,同时也强化了对知识掌握程度。

    2. 更加系统的知识体系

    互联网面试不会问单一的知识(如:算法),而是一次综合性考察。因此我们在备战面试过程中各个知识点都需要准备,如计算机操作系统、计算机网络、数据库、算法、数据结构等。而知识之间不可能孤立存在,而是彼此存在联系。《高效学习》中提到,学习过程分为几个阶段:知识获取,理解,拓展(或者建立联系),纠错,应用,记忆,测试。我们根据面试要求从相关书籍中获取知识理解一些知识和概念,及背后的原理(如什么是面向对象);同时建立起与其他知识之间的联系,例如java虚拟机的垃圾回收算法是什么,性能如何?优缺点是什么?—java虚拟机和算法数据结构的联系。java虚拟机如何做到跨平台,与计算机系统之间是怎样的关系等。通过面试过程纠正我们对知识的错误认知。通过反复纠正、记忆便掌握更加牢固。

    面试中的问题都会将一些知识联系起来,比如MySql索引的数据结构是什么?性能如何?不同索引之间的比较如何?这些都不是单纯的考察MySql知识,同时也在考察对数据库中B+树、hash的理解。

    可以通过复习建立起各个知识之间的联系,并且我们能够花大量时间系统的学习各个方面的知识。通过这样一次复习后,我们拥有了更加系统的互联网知识体系,建立起各知识点之间的联系。

    3. 弥补不足,提升自我

    面试一个非常好的地方就是能够帮助自己,找出自己的不足,这些不足包括知识和沟通等方面。面试中遇到不会的问题便是我们成长最大、最快的地方。这些问题之前学习的时候是否认真考虑过?是否做项目时,某些问题是否有深入地分析过?用的框架是否知其然而知其所以然?总结面试中回答不足的问题,我们在复习过程中重点攻克,查缺补漏,进一步巩固之前的基础知识。虽然面试过程中有时候会被虐的体无完肤,虐得心力憔悴。但是,这也帮助我们找到自己知识的不足,便于弥补和提高。面试过程中最怕的不是被拒绝,而是不知道为什么被拒绝。被拒绝后还不采取相应的措施去做出改变,那即便面再多也是徒然。

    4. 更加深入地了解知识背后原理

    通常我们掌握的知识,仅仅是这个知识,处于一种见树而不见森林的状态,对其别后的原理和联系都不甚了解。于是面试中一旦面试官深入问询时,自己便回答不上了。面试带来的好处就是让我们有更多的机会去反思和思考,让我们能够更加深入地去挖掘知识背后的原理,而不是简简单单地浮于表面的理解。面试中你会发现那些习以为常的知识背后隐藏着一个丰富多彩的知识世界,一花一世界,一叶一森林,这些知识背后总有那些需要了解的知识原理和知识间的联系。通过面试,我们加强对这些知识的理解和学习,通过深入分析背后的原理和建立起知识点的练习,使自己能够做到见树即见森林

    面试是一次知识提升的机会,面试中应该学会总结自己的不足,并针对不足之处深入研究、分析才能促进自己的成长。而不应该仅仅将面试准备当做应付考试一样准备,背背网上的一些所谓的”标准答案“。

    缘分与心态

    我们如此看重实习和工作,是因为进入BAT等互联网大型公司实习一方面能够给我们一个平台提高我们的实践能力、学习更多知识技能和拓展我们的视野,另一方面是能在未来找工作时为我们简历加分。而工作更不用说了,十几年的学习生涯也是为了毕业时找份好工作,为未来奠定一个好的开端。很多人常说第一份工作至关重要,往往决定了一个人未来的发展道路。是否决定一个人未来因个人而异,但是第一份工作还是非常关键,那是我们走入社会的第一步,会对自己个人的成长和视野的开拓产生重要影响。因为不同的公司的文化环境不同,组织结构不同和给个人提供的机会都有所不同。因此也很多人格外看重这份工作。

    而不管是找实习还是找工作,我们都会面临一个问题,那就是面试。面试不仅是考验的是技术和知识,同时也是看缘分和心态。

    找工作之前,有一点你必须清楚,就是找工作是一件看缘分的事情,不是你很牛逼,你就一定能进你想进的公司,都是有一个概率在那。比如相同的公司因为面试官不同问的知识点也可能不同,有时候问的刚好你都准备了,有时候偏偏问些你所不会的。如果你基础好,项目经验足,同时准备充分,那么你拿到offer的概率就会比较高;相反,如果你准备不充分,基础也不好,那么你拿到offer的概率就会比较低,但是你可以多投几家公司,这样拿到offer的几率就要大一点,因为你总有运气好的时候。所以,不要惧怕面试,刚开始失败了没什么的,多投多尝试,面多了你就自然能成面霸了。得失心也不要太重,应该放宽心态,应为最后每个人都会有offer的。

    基础备战

    基础这东西,各个公司都很看重,尤其是BAT这种大公司,他们看中人的潜力,他们舍得花精力去培养,所以基础是重中之重。当你的基础好的发指的时候,你的其他东西都不重要了。

    基础无外乎几部分:语言(C/C++或java),操作系统,TCP/IP,数据结构与算法,再加上你所熟悉的领域。当面试多了,你便会发现有很多是常见的问题和知识点,因此本书对那些常见的面试题进行收集和整理,并对一些题目做深入分析,希望能够帮助大家。


    第一章 操作系统

    1.1 生产者与消费者

    背景

    生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。解决生产者/消费者问题的方法可分为两类: 1.采用某种机制保护生产者和消费者之间的同步; 2.在生产者和消费者之间建立一个管道。第一种方式有较高的效率,并且易于实现,代码的可控制性较好,属于常用的模式。第二种管道缓冲区不易控制,被传输数据对象不易于封装等,实用性不强。

    实现方式

    同步方式实现生产者-消费者模型。在Java中一共有四种方法支持同步,其中前三个是同步方法,一个是管道方法。

    • wait() / notify()方法
    • await() / signal()方法
    • BlockingQueue阻塞队列方法
    • PipedInputStream / PipedOutputStream

      wait()/notify() 、blockingQueue方式

    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
    public class ProducerConsumer<T> {
    static class MsgQueueManager<T> {
    /**
    * 消息总队列
    */
    public final BlockingQueue<T> messageQueue;
    MsgQueueManager(BlockingQueue<T> messageQueue) {
    this.messageQueue = messageQueue;
    }
    MsgQueueManager() {
    this.messageQueue = new LinkedBlockingQueue<T>();
    }
    public void put(T msg) {
    try {
    messageQueue.put(msg);
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    }
    public T take() {
    try {
    return messageQueue.take();
    } catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    }
    return null;
    }
    }
    static class Producer extends Thread {
    private MsgQueueManager msgQueueManager;
    public Producer(MsgQueueManager msgQueueManager) {
    this.msgQueueManager = msgQueueManager;
    }
    @Override
    public void run() {
    msgQueueManager.put(new Object());
    }
    }
    static class Consumer extends Thread{
    private MsgQueueManager msgQueueManager;
    public Consumer(MsgQueueManager msgQueueManager) {
    this.msgQueueManager = msgQueueManager;
    }
    @Override
    public void run() {
    Object o = msgQueueManager.take();
    }
    }
    public static void main(String[] args) {
    MsgQueueManager<String> msgQueueManager = new MsgQueueManager<String>();
    for(int i = 0; i < 100; i ++) {
    new Producer(msgQueueManager).start();
    }
    for(int i = 0; i < 100; i ++) {
    new Consumer(msgQueueManager).start();
    }
    }
    }

    1.2 常见面试题

    • 进程和线程的区别。
    • 死锁的必要条件,怎么处理死锁。
    • Window内存管理方式:段存储,页存储,段页存储。
    • 进程的几种状态。
    • IPC几种通信方式。
    • 什么是虚拟内存。
    • 虚拟地址、逻辑地址、线性地址、物理地址的区别。

    1.3进程和线程

    1.1 线程

    概念:线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。

    好处1)易于调度。 2)提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。 3)开销少。创建线程比创建进程要快,所需开销很少。。 4)利于充分发挥多处理器的功能。通过创建多线程进程,每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。

    线程状态:联系java中线程的几种状态 ,java thread的运行周期中的几种状态,在 java.lang.Thread.State 中有详细定义和说明:

    1. NEW 状态是指线程刚创建, 尚未启动
    2. RUNNABLE 状态是线程正在正常运行中,当然可能会有某种耗时计算/IO等待的操作/CPU时间片切换等,这个状态下发生的等待一般是其他系统资源, 而不是锁, Sleep等
    3. BLOCKED 这个状态下, 是在多个线程有同步操作的场景,比如正在等待另一个线程的synchronized 块的执行释放, 或者可重入的 synchronized块里别人调用wait() 方法,也就是这里是线程在等待进入临界区
    4. WAITING 这个状态下是指线程拥有了某个锁之后, 调用了他的wait方法, 等待其他线程/锁拥有者调用 notify / notifyAll 一遍该线程可以继续下一步操作, 这里要区分 BLOCKED 和 WATING 的区别, 一个是在临界点外面等待进入, 一个是在理解点里面wait等待别人notify, 线程调用了join方法 join了另外的线程的时候, 也会进入WAITING状态, 等待被他join的线程执行结束
    5. TIMED_WAITING 这个状态就是有限的(时间限制)的WAITING, 一般出现在调用wait(long), join(long)等情况下, 另外一个线程sleep后, 也会进入TIMED_WAITING状态
    6. TERMINATED 这个状态下表示该线程的run方法已经执行完毕了,基本上就等于死亡了(当时如果线程被持久持有, 可能不会被回收)

    1.2 进程

    概念:进程是操作系统的概念,每当我们执行一个程序时,对于操作系统来讲就创建了一个进程,在这个过程中,伴随着资源的分配和释放。可以认为进程是一个程序的一次执行过程。

    • 进程与程序的区别
    1. 进程是程序的一次运行活动,属于一种动态的概念。程序是一组有序的静态指令,是一种静态的概念。
    2. 一个进程可以执行一个或多个程序。
    3. 程序可以作为一种软件资源长期保持着,而进程则是一次执行过程,它是暂时的,是动态地产生和终止的。
    4. 进程更能真实地描述并发,而程序不能。
    5. 进程由程序和数据两部分组成,进程是竞争计算机系统有限资源的基本单位
    6. 进程具有创建其他进程的功能;而程序没有。
    7. 进程还具有并发性和交互性,这也与程序的封闭性不同
    • 进程的几种状态:
    1. 创建状态(New):进程正在创建过程中,还不能运行。操作系统在创建状态要进行的工作包括分配和建立进程控制块表项、建立资源表格(如打开文件表)并分配资源、加载程序并建立地址空间表等。
    2. 就绪状态(Ready):进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排人低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。
    3. 执行状态:进程已获得CPU,其程序正在执行。在单处理机系统中,只有一个进程处于执行状态; 在多处理机系统中,则有多个进程处于执行状态。
    4. 阻塞状态:正在执行的进程由于发生某事件而暂时无法继续执行时,便放弃处理机而处于暂停状态,亦即进程的执行受到阻塞,把这种暂停状态称为阻塞状态,有时也称为等待状态或封锁状态。致使进程阻塞的典型事件有:请求I/O,申请缓冲空间等。通常将这种处于阻塞状态的进程也排成一个队列。有的系统则根据阻塞原因的不同而把处于阻塞状态的进程排成多个队列。
    5. 退出状态(Exit):进程已结束运行,回收除进程控制块之外的其他资源,并让其他进程从进程控制块中收集有关信息(如记帐和将退出代码传递给父进程)。
    • 作业(进程)调度算法
    1. 先来先服务调度算法(FCFS) 每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。
    2. 短作业(进程)优先调度算法(SPF) 短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。缺点:长作业的运行得不到保证
    3. 优先权调度算法(HPF) 当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程,这时,又可进一步把该算法分成如下两种。 可以分为: 1.非抢占式优先权算法 2.抢占式优先权调度算法
    4. 高响应比优先调度算法(HRN) 每次选择高响应比最大的作业执行,响应比=(等待时间+要求服务时间)/要求服务时间。该算法同时考虑了短作业优先和先来先服务。
      • 如果作业的等待时间相同,则要求服务的时间愈短,其优先权愈高,因而该算法有利于短作业。
      • 当要求服务的时间相同时,作业的优先权决定于其等待时间,等待时间愈长,其优先权愈高,因而它实现的是先来先服务。
      • 对于长作业,作业的优先级可以随等待时间的增加而提高,当其等待时间足够长时,其优先级便可升到很高,从而也可获得处理机。简言之,该算法既照顾了短作业,又考虑了作业到达的先后次序,不会使长作业长期得不到服务。因此,该算法实现了一种较好的折衷。当然,在利用该算法时,每要进行调度之前,都须先做响应比的计算,这会增加系统开销。
    5. 时间片轮转法(RR) 在早期的时间片轮转法中,系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把CPU分配给队首进程,并令其执行一个时间片。时间片的大小从几ms到几百ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。换言之,系统能在给定的时间内响应所有用户的请求。
    6. 多级反馈队列调度算法 它是目前被公认的一种较好的进程调度算法。
      • 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第i+1个队列的时间片要比第i个队列的时间片长一倍。
      • 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。
      • 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。
    • 作业与进程的区别

      一个进程是一个程序对某个数据集的执行过程,是分配资源的基本单位。作业是用户需要计算机完成的某项任务,是要求计算机所做工作的集合。一个作业的完成要经过作业提交、作业收容、作业执行和作业完成4个阶段。而进程是对已提交完毕的程序所执行过程的描述,是资源分配的基本单位。

      • 作业是用户向计算机提交任务的任务实体。在用户向计算机提交作业后,系统将它放入外存中的作业等待队列中等待执行。而进程则是完成用户任务的执行实体,是向系统申请分配资源的基本单位。任一进程,只要它被创建,总有相应的部分存在于内存中。
      • 一个作业可由多个进程组成,且必须至少由一个进程组成,反过来则不成立。
      • 作业的概念主要用在批处理系统中,像UNIX这样的分时系统中就没有作业的概念。而进程的概念则用在几乎所有的多道程序系统中。

    1.3 进程和线程的关系:

    • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
    • 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
    • 处理机分给线程,即真正在处理机上运行的是线程。
    • 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。线程是指进程内的一个执行单元,也是进程内的可调度实体.

    1.4 进程与线程的区别

    • 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
    • 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
    • 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源.
    • 系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,因此操作系统所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置。而线程切换时只需保存和设置少量寄存器内容,开销很小。此外,由于同一进程内的多个线程共享进程的地址空间,因此,这些线程之间的同步与通信非常容易实现,甚至无需操作系统的干预。
    • 通信方面:进程间通讯有管道、信号量、信号消息队列、socket来维护,而线程间通过通道、共享内存、信号灯来进行通信。

    2. IPC几种通信方式(进程间的通信方式)

    • 管道( pipe ):只支持半双工通信方式,即只能单向传输;只能在父子进程之间使用。

      管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的首端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。

      流管道:去除第一个限制,支持双向传输;

      命名管道:去除第二个限制,可以在不相关进程之间进行通信。

    • 命名管道 (named pipe): 命名管道也是半双工的通信方式,它克服了管道没有名字的限制,并且它允许无亲缘关系进程间的通信。命令管道在文件系统中有对应的文件名,命名管道通过命令mkfifo或系统调用mkfifo来创建。

    • 信号量( semophore ): 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

    • 消息队列( message queue ): 消息队列是由消息的链表结构实现,存放在内核中并由消息队列标识符标识。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

    • 信号 ( sinal ):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。除了用于进程通信外,进程还可以发送信号给进程本身。

    • 共享内存( shared memory ):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。

    • 套接字( socket ): 也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。

    3. 死锁的必要条件和处理死锁。

    死锁概念:两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

    活锁 :活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败

    死锁条件

    • 互斥条件:一个资源每次只能被一个进程使用
    • 不可剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺
    • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
    • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系.

    死锁预防

    • 破坏互斥条件。允许某些进程(线程)同时访问某些资源,但有的资源不允许同时被访问如打印机等。
    • 破坏不可抢占条件:即允许进程强行从占有者那里夺取某些资源。实现起来困难,会降低系统性能。
    • 破坏占有且申请条件。可以实行预先分配策略,即进程在运行前一次性地向系统申请它所需要的全部资源。如果当前进程所需的全部资源得不到满足,则不分配任何资源。只有当系统能够满足当前的全部资源得到满足时,才一次性将所有申请的资源全部分配给该进程。由于运行的进程已占有了它所需的全部资源,所以不会发生占有资源又重新申请资源的现象,因此不会发生死锁。但是有以下缺点:

      • 由于进程在执行时是动态的,不可预测的,进程在执行之前不可能知道它所需的全部资源。

      • 资源利用率低。无论所分配资源何时用到,一个进程只有在占有所需的全部资源后才能执行。即使有些资源最后才被该进程用到一次,但该进程在生存期间一直占有它们,造成长期占有。

      • 降低了进程的并发性。因为资源有限,又加上存在浪费,能分配到所需全部资源的进程个数必然少了。

    • 破坏循环等待条件。实行资源有序分配策略。采用这种策略即把资源事先分类编号,按号分配。所有进程对资源的请求必须严格按资源需要递增的顺序提出。进程先占用小号资源,才能申请大号资源,就不会产生环路。这种策略与前面的策略相比,资源的利用率和系统吞吐量都有很大提高,但是也存在缺点:限制了进程对资源的请求,同时系统给所有资源合理编号也是件困难事,并增加了系统开销。

    死锁的避免:

    • 银行家算法:该算法需要检查申请者对资源的最大需求量,如果系统现存的各类资源可以满足申请者的请求,就满足申请者的请求。这样申请者就可很快完成其计算,然后释放它占用的资源,从而保证了系统中的所有进程都能完成,所以可避免死锁的发生。

    死锁的解除:

    • 资源剥夺法。挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。
    • 撤销进程法。强制撤销部分、甚至全部死锁进程并剥夺这些进程的资源。
    • 进程回退法。让一(多)个进程回退到足以回避死锁的地步,进程回退时自愿释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。

    死锁检测与死锁恢复

    ​ 如果一个进程所请求的资源能够被满足,那么就让它执行,否则释放它拥有的所有资源,然后让其它能满足条件的进程执行。

    1.4 Window内存管理方式:页存储、段存储、段页存储。

    1.4.1 分页存储管理

    原理:将程序的逻辑地址空间划分为固定大小的页(page),而物理内存划分为同样大小的页框(page frame)或物理块,每个物理块的大小一般取2的整数幂。程序加载时,可将任意一页放人内存中任意一个页框,这些页框不必连续,从而实现了离散分配。该方法需要CPU的硬件支持,来实现逻辑地址和物理地址之间的映射。在页式存储管理方式中地址结构由两部构成,前一部分是页号,后一部分为页内地址w(位移量)。

    逻辑地址-物理地址转换:CPU中的内存管理单元(MMU)按逻辑页号通过查进程页表得到物理页框号,将物理页框号与页内地址相加形成物理地址。

    优点

    1)没有外部碎片,提高内存的利用率。 2)一个程序不必连续存放。3)程序内存可以动态变化。

    缺点 : 1)页内部碎片,造成浪费。2)页长与程序的逻辑大小不相关。 3)不利于编程时的独立性,给换入换出、存储保护和存储共享等操作造成麻烦。

    1.4.2 分段存储

    思想 :将用户程序地址空间分成若干个大小不等的段,每段可以定义一组相对完整的逻辑信息。存储分配时,以段为单位,段与段在内存中可以不相邻接,也实现了离散分配。通常,程序员把子程序、操作数和常数等不同类型的数据划分到不同的段中(写c程序时会用到),并且每个程序可以有多个相同类型的段。

    在为某个段分配物理内存时,可以采用首先适配法、下次适配法、最佳适配法等方法。在回收某个段所占用的空间时,要注意将收回的空间与其相邻的空间合并。

    地址映射:逻辑地址由段号和段内地址两部分组成。为了完成进程逻辑地址到物理地址的映射,处理器会查找内存中的段表,由段号得到段的首地址,加上段内地址,得到实际的物理地址。

    优点:(1)段的逻辑独立性使其易于编译、管理、修改和保护,也便于多道程序共享。 (2)段长可以根据需要动态改变,允许自由调度,以便有效利用主存空间。 (3)方便编程,分段共享,分段保护,动态链接,动态增长

    缺点: (1)主存空间分配比较麻烦。 (2)有外部碎片。

    1.4.3 分页和分段的主要区别

    • 页是信息的物理单位,分页是为实现离散分配方式,以消减内存的外零头,提高内存的利用率;段则是信息的逻辑单位,它含有一组其意义相对完整的信息,分段的目的是为了能更好地满足用户的需要。
    • 页的大小固定且由系统决定,由系统把逻辑地址划分为页号和页内地址两部分,是由机器硬件实现的;而段的长度却不固定,决定于用户所编写的程序和编译器。
    • 分页的作业地址空间是一维的,即单一的线性地址空间,程序员只需利用一个记忆符,即可表示一个地址;而分段的作业地址空间则是二维的,程序员在标识一个地址是,即需给出段名,又需给出段内地址。
    • 分页信息很难保护和共享、分段存储按逻辑存储所以容易实现对段的保存和共享。

    1.4.4 段页存储

    原理:程序员按照分段系统的地址结构将地址分为段号与段内位移量,地址变换机制将段内位移量分解为页号和页内位移量。

    实现 :为实现段页式存储管理,系统应为每个进程设置一个段表,包括每段的段号,该段的页表始址和页表长度。每个段有自己的页表,记录段中的每一页的页号和存放在主存中的物理块。

    将程序按逻辑结构划分为若干个逻辑段,然后再将每个逻辑段划分为若干个大小相等的逻辑页。每个程序段对应一个段表,每页对应一个页表。主存空间也划分为若干个物理页。

    地址变换

    1) 利用段表始址和段号来求出该段所对应的段表项在段表中的位置,从中得到该段的页表始址

    2) 利用逻辑地址中的段内页号P来获得对应页的页表项位置,从中读出该页所在的物理块号b

    3)再利用块号b和页内地址来构成物理地址。

    优缺点 : 段页存储管理方式综合了段式管理和页式管理的优点,但需要经过两级查表才能完成地址转换,消耗时间多。

    • 优点 (1) 它提供了大量的虚拟存储空间。 (2) 能有效地利用主存,为组织多道程序运行提供了方便。
    • 缺点: (1) 增加了硬件成本、系统的复杂性和管理上的开消。 (2) 存在着系统发生抖动的危险。 (3) 每段最后一页可能有内部碎片。

    1.5 虚拟内存。

    • 物理内存: 真实硬件内存可用大小,CPU的地址线可以直接进行寻址的内存空间大小。
    • 虚拟内存: 它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。

    1.6 操作系统如何进行分页调度

    • 先进先出算法(FIFO):在主存中停留时间最长(即最老)的一页置换,即先进入内存的页,先退出内存。
    • 最优置换算法(OPT):所选择的老页应是将来不再被使用,或者是在最远的将来才被访问。采用这种页面置换算法,保证有最少的缺页率。 但是最优页面置换算法的实现是困难的,因为它需要人们预先就知道一个进程整个运行过程中页面走向的全部情况。
    • LRU(最近最少使用):如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。
    • 时钟算法: 又称为最近未用(Not Recently Used, NRU)算法。

    1.7 磁盘调度算法:

    • 先来先服务(FCFS),按访问请求到达的先后顺序服务。简单,公平,但是效率不高,相临两次请求可能会造成最内到最外柱面寻道,使磁头反复移动,增加了服务时间,对机器不利。
    • 最短寻道时间优先(SSTF),优先选择距当前磁头最近的访问请求进行服务,主要考虑寻道优先。改善了磁盘平均服务时间,但是造成某些访问请求长期等待得不到服务。
    • 扫描算法(SCAN)当有访问请求时,磁头按一个方向移动,在移动过程中对遇到的访问请求进行服务.
    • 循环扫描算法(CSCAN):磁臂改为单项移动,由外向里。当前位置开始沿磁臂的移动方向去选择离当前磁臂最近的哪个柱面的访问者。如果沿磁臂的方向无请求访问时,再回到最外,访问柱面号最小的作业请求。

    第二章 计算机网络

    2.1 OSI(开放系统互联参考模型)标准模型

    物理层 负责为数据端设备透明地传输原始比特流,并且定义了数据终端设备和数据通信设备的物理和逻辑链接方法。传输单位是比特。 协议:RJ45、CLOCK、IEEE802.3 设备:(中继器,集线器)

    数据链路层 将网络层传下来的IP数据报组装成帧,并检测和矫正物理层产生的传输差错,使得链路对网络层显示一条无差错、可靠的数据传输线路。功能可以概括为成帧,差错控制、流量控制和传输管理 协议有:HDLC(高级数据链路控制协议),PPP,STP,SDLC,CSMA(载波监听多路访问) 设备:(网桥,交换机)

    网络层 负载在网络层上将数据封装成数据报,将数据报从源端传到目的端,同时进行路由选择,为分组交换网上的不同主机提供通信服务。关键问题是对分组进行选择,并实现流量控制、拥塞控制、差错控制和网际互联等功能。传输单位数据报。 协议: IP,ICMP(因特网控制报文协议),IGMP(因特网组管理协议),ARP,RARP,OSPF(开放最短路径优先),IPX 设备:路由器

    传输层 负责主机中两个进程之间的通信,为端到端连接提供可靠的传输服务。为端到端连接提供流量控制、差错控制、服务质量、数据传输管理等服务。 协议:TCP,UDP

    会话层 会话层允许不同主机上各个进程之间的会话,会话层利用传输层提供的端到端的服务,向表示层提供它的增值服务。这种服务主要是为表示层实体或用户进程建立连接并在连接上提供有序地传输数据。 协议:SQL、RPC(远程调用协议)

    表示层 用于处理两个通信系统中交换信息的表示方式。如数据压缩,加密和解密等。 协议:JPEG、MPEG、ASII

    应用层 是TCP/IP的最高层,它是直接为应用进程服务的一层。当不同的应用进程数据通信或数据交换时,就去调用应用层的不同协议实体,让这些实体去调用TCP或者UDP层服务来进行网络传输。 协议:FTP(21) TELNET(23) SMTP(25) DNS(53) TFTP(69) HTTP(80) SNMP(161),DHCP(动态主机配置协议)

    TCP/IP 分层

    1)网络接口层(接收和发送数据报) 负责将数据报发送到网络介质上,以及从网络上接收TCP/IP数据报,相当于OSI的物理层和数据层。 2)网际层(数据报封装和路由寻址功能) 主要负责寻址和对数据报的封装以及重要的路由选择功能。 3)传输层 负责在应用进程之间的“端到端”的通信,即从某个应用进程传输到另一个应用进程。 4)应用层 是TCP/IP的最高层,它是直接为应用进程服务的一层。当不同的应用进程数据通信或数据交换时,就去调用应用层的不同协议实体,让这些实体去调用TCP或者UDP层服务来进行网络传输。

    五层体系结构模型:物理层,数据链路层,网络层,传输层,应用层

    2.2 IP地址分类

    A类地址:以0开头, 第一个字节范围:1~126(1.0.0.0 - 126.255.255.255); B类地址:以10开头, 第一个字节范围:128~191(128.0.0.0 - 191.255.255.255); C类地址:以110开头, 第一个字节范围:192~223(192.0.0.0 - 223.255.255.255); D类地址:1110开头, 224~239 E类地址:11110开头, 240~255 主机号全为0表示本网络本身 ,主机号全为1表示本网络广播地址 。

    127.0.0.0 环路自检地址,32个0 表示本网络的本主机 32个1表示整个TCP/IP网络的广播地址,等效本网络的广播地址。 专用地址: 10.0.0.0—10.255.255.255, 172.16.0.0—172.31.255.255, 192.168.0.0—192.168.255.255。(Internet上保留地址用于内部) IP地址与子网掩码相与得到网络号

    2.3 ARP地址解析协议

    本局域网上各主机和路由器的IP地址到MAC地址的映射表,称为ARP表。使用ARP协议动态维护此表。 ARP工作在网络层中,其工作原理是:当主机A欲向本局域网上的某个主机B发送IP数据报时,就先在其ARP高速缓存中查看有无主机B的IP地址。如果有可以查出其对应的硬件地址,再将此硬件地址写入MAC帧,然后通过局域网将该MAC帧发往此硬件地址。如果没有,就通过使用目的MAC地址为本网络的广播地址即32个1的帧来封装并广播ARP请求分组,可以使同一个局域网里的所有主机收到ARP请求。当主机B收到该ARP请求后,就会向主机A发出响应ARP分组,分组中包含主机B的IP与MAC地址的映射关系,主机A在收到后将此映射写入ARP缓存中,然后按查询到的硬件地址发送MAC帧。

    ARP是解决同一个局域网上主机与路由器的IP地址和硬件地址的映射问题。如果所要找的主机和源主机不在同一个局域网上,那么通过ARP协议找到一个位于本局域网上的某个路由器硬件地址,然后把分组发送给这个路由器,让这个路由器把分组转发给下一个网络。

    2.4 交换机、路由器、网关

    路由器是连接不同的网络并完成路由转发。

    交换机:是多个端口的网桥,工作在数据链路上,将两个或多个以太网连接起来成为更大的以太网。它能将网络分成小的冲突域,为每个工作站提供更高的带宽。其原理是,检测从以太端口来的数据帧的源和目的地的MAC地址,然后与系统内部的动态查找表进行比较。若数据帧的MAC地址不在查找表中,则将该地址加入查找表中,并将数据帧发送给相应的端口。

    区别: 路由器:工作在网络层,是能够连接不同的广域网形成更大的广域网。连接的是异构网络。根据IP地址转发。 交换机:工作在数据链路层,是将以太网连接形成更大的以太网,同一个网络。根据MAC地址进行转发。

    网关(Gateway) 仅用于两个高层协议不同的网络互连。网关既可以用于广域网互连,也可以用于局域网互连。 在不同的通信协议、数据格式或语言,甚至体系结构完全不同的两种系统之间,网关是一个翻译器。

    2.5 OSI 和TCP/IP的区别

    1. OSI精确定义了服务、协议和接口,符合面向对象程序设计思想。而TCP/IP在这些概念上没有明确区分。
    2. TCP/IP先有的协议,是事实标准。
    3. TCP/IP考虑了异构网的互联问题,而OSI只考虑用一种标准的公用数据网将各种不同系统互连。

    2.6 TCP与UDP的区别

    1、TCP面向连接;UDP是无连接的,即发送数据之前不需要建立连接 2、TCP提供可靠全双功的通信服务。UDP是半双功,只能单向传播。 3、通过TCP连接可靠传送的数据,可靠的、无差错,不丢失,不重复,且按序到达;UDP则是不可靠信道,尽最大努力交付,即不保证可靠交付. 4、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的. 5、TCP具有拥塞控制,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等) 6、每一条TCP连接只能是点到点的;UDP比较灵活,支持一对一,一对多,多对一和多对多的交互通信 7、TCP首部开销20字节;UDP的首部开销小,只有8个字节.

    TCP应用场景:效率要求相对低,但对准确性要求相对高的场景。如文件传输(准确高要求高、但是速度可以相对慢)、接受邮件、远程登录。

    • FTP协议,21端口,文件传输
    • ssh: 22端口,远程登录及其他服务
    • Telnet:23端口,远程登录,明文传输,不安全。
    • SMTP: 25,发送邮件 POP3, 110,邮件接收。
    • HTTP:80,web文本传输协议, HTTPS, 443.

    UDP应用场景:效率要求相对高,对准确性要求相对低的场景。如:QQ聊天、在线视频、网络语音电话、广播通信。

    • DNS(53):用于域名解析服务,将域名地址转换为IP地址。
    • RIP(520):路由信息协议
    • SNMP(161):简单网络管理协议,使用161号端口,是用来管理网络设备的。
    • TFTP(69):简单文件传输协议。

    2.7 常见的路由选择协议,以及它们的区别

    常见的路由选择协议有:RIP协议、OSPF协议。 RIP协议(路由信息协议):底层是贝尔曼福特算法(Bellman-Ford),基于距离向量的路由选择协议,它选择路由的度量标准(metric)是跳数,最大跳数是15跳,如果大于15跳,它就会丢弃数据包。仅和相邻路由器交换当前路由器所知道的全部信息。是应用层协议,使用UDP传输数据,端口520

    OSPF协议(开放最短路由优先):底层是迪杰斯特拉(Dijkstra)算法,是链路状态路由选择协议,它选择路由的度量标准是带宽,延迟。向本自治系统中所有路由器发送与本路由器相邻的所有路由器的链路状态,但这只是路由器知道的部分信息。网络层协议,直接IP数据报传输. 端口89

    2.8 TCP的三次握手与四次挥手过程

    第一次握手:A向B发送一个连接请求报文段,其首部中同步位SYN被设置为1,序号seq=x,x为A随机生成的序号;

    第二次握手:B如果同意建立连接,就向A发回确认,并为该TCP连接分配缓存和变量。在确认报文段中,SYN和ACK位都被设置为1,确认号字段值为ack=x+1,并且服务器随机产生起始序号seq=y。

    第三次握手:当客户机A收到确认报文段后,还要向服务器给出确认,并且也要给该连接分配缓存和变量。这个报文段的确认位ACK被设置为1,序号段被设置为seq=x+1,确认号字段ack=y+1. 该报文段可以携带数据,如果不携带数据则不消耗序号。

    第一次挥手:A向B发送连接释放报文,并停止再发送数据,主动关闭TCP连接。结束标志位FIN被设置为1,seq=u,它等于前面已经发送过的数据的最后一个字节的序号加1。

    第二次挥手:B向A发出确认报文,确认号是ack=u+1,序号为v,等于它前面已经发送过的数据的最后一个字节序号加1.此时客户机到服务器这个方向的连接就释放了,TCP处于半关闭状态。ACK=1,seq=v,ack=u+1

    第三次挥手:若服务器B已经没有要向客户机A发送的数据,就通知TCP释放连接,此时发出FIN=1,确认号ack=u+1,序号seq=w,已经发送过的数据最后一个字节加1。确认为ACK=1. (FIN=1, ACK=1, seq=w, ack=u+1)

    第四次挥手:A客户机收到连接释放报文段后,必须向B发出确认。在确认报文段中,确认位ACK=1,序号seq=u+1,确认号ack=w+1. 此时连接还没有释放掉,必须经过时间等待计时器设置的时间2MSL(Max Segment Lifetime),后,客户机才进入连接关闭状态。 (ACK=1,seq=u+1,ack=w+1)

    不使用二次握手?防止因为网络延迟,导致服务端响应客户端的多个请求,并为其建立多个连接,浪费服务器资源。

    为什么四次挥手,主动方要等待2MSL后才关闭连接? 保证TCP协议的全双工连接能够可靠关闭.

    2.9 TCP拥塞控制

    拥塞控制 :使用四种算法:慢开始,拥塞避免,快重传,快恢复。 TCP要求发送端维护两个窗口: 1) 接收窗口rwnd,接收方根据当前缓存大小所许诺的最新窗口值。 2) 拥塞窗口 cwnd ,发送方根据自己估算的网络拥塞程度而设置的窗口值。 发送窗口的上限是取这两者的最小值。

    • 慢开始: TCP刚连接好时,先令拥塞窗口cwnd =1 ,在每次收到一个对新报文段的确认时将cwnd加倍(因此是成倍增长2的n次方). cwnd的大小呈指数增长。
    • 拥塞避免算法: 当cwnd大于等于慢开始门限ssthresh时,cwnd窗口每次加1而不是加倍。当发送方检测到超时事件的发生时,就将慢开始门限设置为当前cwnd的一半,同时将cwnd设置为1. 这样的目的是迅速减少主机发送到网络的分组数,使得发生拥塞的路由器有足够的时间吧队列中积压的分组处理完毕。
    • 快重传:当发送方连续收到三个重复的ACK报文时,直接重传对方尚未收到的报文段,而不必等待那个报文段设置的重传计时器超时。
    • 快恢复:当发送端收到连续三个冗余的ACK时,就执行“乘法减小”算法,把慢开始门限ssthresh减半,cwnd设置为慢开始门限减半后的数值(与慢开始不同)。

    TCP滑动窗口与回退N针协议

    滑动窗口: 发送方都维持一组连续的允许发送的帧的序号称为发送窗口。同时接收方也维持一组连续的允许接收的帧序号,称为接收窗口。发送窗口是用来对发送方进行流量控制,接收窗口是用来控制接收那些数据帧不可以接收那些帧。 在发送端,收到一个确认帧,发送窗口就向前滑动一个帧位置,当发送窗口没有可以发送的帧时,发送方就停止发送。直到接收方发送的确认帧使发送窗口向前移动。 在接收端,只有收到数据帧的序号落在接收窗口内才将该帧收下,否则一律丢弃。每收到一个帧后就发送回确认帧。

    后退N帧协议 :发送窗口大于1,接收窗口等于1.在后退N帧中,发送方不需要收到上一帧的ACK后才能发送下一帧,而是可以连续发送帧。当接收方检测出失序信息帧后,要求发送方重发最后一个正确接收的帧之后的所有未被确认的帧。源站每发完一帧就要为该帧设置超时计时器,如果在超时时间内没有收到确认帧则进行重发。服务端会采用累积确认的方式,不是每个帧都发确认,可以连续收到好几个正确帧后发回一个确认信息。接收方因为窗口为1,所以必须按序接收数据帧,如果某个序大于当前所期望的序号时就会连续发送3个ACK确认帧,要求客户端重传失序帧。

    TCP的可靠性

    TCP的可靠性是通过顺序编号和确认(ACK)来实现的。握手与断开都需要通讯双方确认,数据传输也需要双方确认成功,在协议中还规定了:分包、重组、重传等规则。

    2.10 HTTP

    Http request类型:HTTP协议中共定义了八种方法或者叫“动作”来表明对Request-URI指定的资源的不同操作方式。GET, 向特定的资源发出请求。 POST:向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的创建和/或已有资源的修改。 PUT:向指定资源位置上传其最新内容。 DELETE:请求服务器删除Request-URI所标识的资源。 HEAD:请求读取由URL所标志的信息的首部。 OPTIONS:返回服务器针对特定资源所支持的HTTP请求方法。 TRACE:回显服务器收到的请求,主要用于测试或诊断。 CONNECT:HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。

    Http1.1和Http1.0的区别

    1. HTTP1.0 是短连接,HTTP1.1是长连接,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。一个包含有许多图像的网页文件的多个请求和应答可以在一个连接中传输。头部字段Connection: keep-alive
    2. HTTP 1.1中增加Host请求头字段后,实现了在IP端口上创建多个虚拟WEB站点。
    3. HTTP 1.1还提供了与身份认证、状态管理和Cache缓存等机制相关的请求头和响应头
    4. 带宽优化。HTTP/1.1中在请求消息中引入了range头域,它允许只请求资源的某个部分。
    5. HTTP/1.1增加了OPTIONS方法,它允许客户端获取一个服务器支持的方法列表。

    Cookie与Session

    Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端记录信息确定用户身份。

    Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就产生一个用户身份标识,然后在响应消息中将该标识号以Cookie的形式传递给浏览器.客户端浏览器会把Cookie保存起来。浏览器在以后每次访问该web服务器时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容. Cookie不能被浏览器共享 Cookie具有不可跨域名性,Cookie的maxAge决定着Cookie的有效期,单位为秒(Second) ,默认情况下用户退出浏览器后被删除。

    Session是另一种记录客户状态的机制,保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上,为了提高存取速度,服务器一般把Session放在内存里,每个用户都会有一个独立的Session ID, session也存在过期时间,通过cookie会刷新session的访问。

    访问一个网页的过程,协议:DNS、HTTP、TCP、OSPF、IP、ARP

    1 浏览器向DNS请求url的服务器IP地址, 2) 域名系统DNS解析出IP地址 3) 浏览器与该服务器建立TCP连接 4) 通过HTTP获取内容

    DNS域名解析协议,简单描述其工作原理。

    域名解析是把域名映射成IP地址或者把IP地址映射成域名的过程。

    域名解析有两种方式:递归查询和迭代查询。 递归查询时,DNS请求客户身份有变化,迭代查询时,请求客户一直是本地主机。

    2.11 各种协议

    ICMP协议: 因特网控制报文协议。它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。

    NAT协议:内网地址转换协议,是一种将私有(保留)地址转化为合法IP地址的转换技术。

    DHCP协议:一个局域网的网络协议,使用UDP协议工作,用途:给内部网络或网络服务供应商自动分配IP地址,给用户或者内部网络管理员作为对所有计算机作中央管理的手段。


    第三章 算法与数据结构

    3.0 知识点汇总

    1. 链表与数组。
    2. 队列和栈,出栈与入栈。
    3. 链表的删除、插入、反向。
    4. 字符串操作。
    5. Hash表的hash函数,冲突解决方法有哪些。
    6. 各种排序:冒泡、选择、插入、希尔、归并、快排、堆排、桶排、基数的原理、平均时间复杂度、最坏时间复杂度、空间复杂度、是否稳定。
    7. 快排的partition函数与归并的Merge函数。
    8. 对冒泡与快排的改进。
    9. 二分查找
    10. 二叉树、B+树、AVL树、红黑树、哈夫曼树。
    11. 二叉树的前中后续遍历:递归与非递归写法,层序遍历算法。
    12. 图的BFS与DFS算法,最小生成树prim算法与最短路径Dijkstra算法。
    13. KMP算法。
    14. 排列组合问题。
    15. 动态规划、贪心算法、分治算法。
    16. 大数据处理:类似10亿条数据找出最大的1000个数………等等

    3.1 排序算法

    1.直接插入排序

    思想:每次将一个待排序的数据按照其关键字的大小插入到前面已经排序好的数据中的适当位置,直到全部数据排序完成。时间复杂度:O(n^2) O(n) O(n^2) (最坏 最好 平均) 空间复杂度:O(1) 稳定性: 稳定 每次都是在前面已排好序的序列中找到适当的位置,只有小的数字会往前插入,所以原来相同的两个数字在排序后相对位置不变。 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /**
    * 插入排序
    * @param array
    */
    public static void insertSort(int[] array) {
    for (int i = 2; i < array.length; i++ ) {
    int val = array[i];
    int j = i -1;
    while (j >= 0 && array[j] > val) { // array[j] > val
    array[j+1] = array[j];
    j--;
    }
    array[j+1] = val; // array[j+1] 不是array[j]
    }
    }

    2.希尔排序

    思想:希尔排序根据增量值对数据按下标进行分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整体采用直接插入排序得到有序数组,算法终止。 时间复杂度:O(n2) O(n) O(n1.5) (最坏,最好,平均) 空间复杂度:O(1) 稳定性:不稳定 因为是分组进行直接插入排序,原来相同的两个数字可能会被分到不同的组去,可能会使得后面的数字会排到前面,使得两个相同的数字排序前后位置发生变化。 不稳定举例: 4 3 3 2 按2为增量分组,则第二个3会跑到前面

    代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public static void shellSort(int[] array) {
    int len;
    len = array.length / 2; // 分成n/2组
    while (len >= 1) {
    for (int i = len; i < array.length; ++i) { //对每组进行直接插入排序
    int temp = array[i];
    int j = i - len;
    while (j >= 0 && array[j] > temp) {
    array[j + len] = array[j];
    j -= len;
    }
    array[j + len] = temp;
    }
    len /= 2;
    }
    }

    交换排序

    3.冒泡排序

    思想:对待排序元素的关键字从后往前进行多遍扫描,遇到相邻两个关键字次序与排序规则不符时,就将这两个元素进行交换。这样关键字较小的那个元素就像一个泡泡一样,从最后面冒到最前面来。 时间复杂度:最坏:O(n2) 最好: O(n) 平均: O(n2) 空间复杂度:O(1) 稳定性:稳定,相邻的关键字两两比较,如果相等则不交换。所以排序前后的相等数字相对位置不变。

    代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public static void bubbleSort(int[] array) {
    boolean flag; // 用来判断当前这一轮是否有交换数值,若没有则表示已经排好许了
    for (int i = 0; i < array.length; i++) {
    flag = false;
    /**
    * 这边要注意 for (int j = array.length -1; j >= i + 1; j--)。 不要写成
    * for (int j = i + 1; j < array.length ; j++)
    */
    for (int j = array.length -1; j >= i + 1; j--) {
    if (array[j -1 ] > array[j]) {
    //数据交换
    int temp = array[j - 1];
    array[j - 1] = array[j];
    array[j] = temp;
    //设置标志位
    flag = true;
    }
    }
    if (!flag) {
    break;
    }
    }
    }

    4.快速排序

    思想:该算法是分治算法,首先选择一个基准元素,根据基准元素将待排序列分成两部分,一部分比基准元素小,一部分大于等于基准元素,此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地排序划分的两部分。基准元素的选择对快速排序的性能影响很大,所有一般会想打乱排序数组选择第一个元素或则随机地从后面选择一个元素替换第一个元素作为基准元素。 时间复杂度:最坏:O(n2) 最好: O(nlogn) 平均: O(nlogn) 空间复杂度:O(nlogn)用于方法栈 稳定性:不稳定 快排会将大于等于基准元素的关键词放在基准元素右边,加入数组 1 2 2 3 4 5 选择第二个2 作为基准元素,那么排序后 第一个2跑到了后面,相对位置发生变化。

    代码

    1
    2
    3
    public static void quickSort(int[] array) {
    partition(array, 0, array.length - 1);
    }

    选择排序

    5.直接选择排序

    思想:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后每次从剩余未排序元素中继续寻找最小(大)元素放到已排序序列的末尾。以此类推,直到所有元素均排序完毕 时间复杂度:最坏:O(n^2) 最好: O(n^2) 平均: O(n^2) 空间复杂度:O(1) 稳定性:不稳定 例如数组 2 2 1 3 第一次选择的时候把第一个2与1交换使得两个2的相对次序发生了改变。

    代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public static void selectSort(int[] array) {
    for (int i = 0; i < array.length; i++) {
    int minIdx = i;
    for (int j = i + 1; j < array.length; j++) {
    if (array[j] < array[minIdx]) {
    minIdx = j;
    }
    }
    exch(array, i, minIdx);
    }
    }

    6.堆排序

    思想:堆排序是利用堆的性质进行的一种选择排序,先将排序元素构建一个最大堆,每次堆中取出最大的元素并调整堆。将该取出的最大元素放到已排好序的序列前面。这种方法相对选择排序,时间复杂度更低,效率更高。 时间复杂度:最坏:O(nlog2n) 最好: O(nlog2n) 平均: O(nlog2n) 空间复杂度:O(1) 稳定性:不稳定 例如 5 10 15 10。 如果堆顶5先输出,则第三层的10(最后一个10)的跑到堆顶,然后堆稳定,继续输出堆顶,则刚才那个10跑到前面了,所以两个10排序前后的次序发生改变。

    代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 第一个元素没有利用
    public static void heapSort(int[] array) {
    int N = array.length -1;
    for (int k = N / 2; k >= 1; k--) { // k >= 1
    sink(array, k, N);
    }
    while (N > 1) {
    // 最大堆, 选择最大值放在最后
    exch(array, 1, N --);
    sink(array, 1, N);
    }
    }
    private static void sink(int[] array, int k, int N){
    while (2 * k <= N) {
    int j = 2 * k;
    if (j < N && array[j] < array[j+1]) { // <
    j++;
    }
    if (array[j] < array[k]) break; // <
    exch(array, k, j);
    k = j;
    }
    }

    7.归并排序

    思想:归并排序采用了分治算法,首先递归将原始数组划分为若干子数组,对每个子数组进行排序。然后将排好序的子数组递归合并成一个有序的数组。 时间复杂度:最坏:O(nlog2n) 最好: O(nlog2n) 平均: O(nlog2n) 空间复杂度:O(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
    30
    31
    32
    33
    public static void mergeSort(int[] array) {
    sort(array, 0, array.length - 1);
    }
    private static void sort(int[] array, int left, int right) {
    if (left < right) {
    int middle = (left + right) >> 1;
    //递归处理相关的合并事项
    sort(array, left, middle);
    sort(array, middle + 1, right);
    merge(array, left, middle, right);
    }
    }
    private static void merge(int[] array, int lo, int mid, int hi) {
    //创建一个临时数组用来存储合并后的数据
    int[] temp = new int[array.length];
    int left = lo;
    int right = mid + 1;
    int k = lo;
    while (left <= mid && right <= hi) {
    if (array[left] < array[right])
    temp[k++] = array[left++];
    else
    temp[k++] = array[right++];
    }
    //处理剩余未合并的部分
    while (left <= mid) temp[k++] = array[left++];
    while (right <= hi) temp[k++] = array[right++];
    //将临时数组中的内容存储到原数组中
    while (lo <= hi) array[lo] = temp[lo++];
    }

    3.2 查找

    1. 顺序查找

    思路:这是最简单的算法,从头开始遍历每个元素,并将每个元素与查找元素比较,如果一致则返回。 时间复杂度: O(N) 空间复杂度: O(1)

    代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public int search(int[] array, int num) {
    if(array == null || array.length == 0) {
    return -1;
    }
    for(int i = 0; i < array.length; i++) {
    if (array[i] == num) {
    return i;
    }
    }
    return -1;
    }

    2. 二分查找

    思路:二分查找前提是查找的数组是有序的,利用数据有序的特性提高查找性能。首先与数组中间位置的值比较,如果查找值大于中间位置值,则对数组右边以相同的思路查找,否则在左边以相同方式查找。这种方式使得每次查找范围变为原来的1/2. 时间复杂度: O(log2n) 空间复杂度: O(1)

    代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public int halfSearch(int[] array, int num) {
    if(array == null || array.length == 0) {
    return -1;
    }
    int lo = 0, hi = array.length-1;
    while(lo <= hi) {
    int mid = (lo + hi) >> 2;
    if (array[mid] == num) {
    return mid;
    } else if (array[mid] < num) {
    hi = mid -1;
    } else {
    lo = mid + 1;
    }
    }
    return -1;
    }

    3. hash 算法

    思想:哈希表是根据设定的哈希函数H(key)处理冲突方法将一组关键字映射到一个有限的地址区间上,并将关键字对应的值存储在该地址空间,可以通过关键字快速获取对应的值,这种表称为哈希表或散列,所得存储位置称为哈希地址或散列地址。作为线性数据结构与表格和队列等相比,哈希表无疑是查找速度比较快的一种。 查找复杂度: O(1)

    哈希函数寻址

    1. 直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a?key + b,其中a和b为常数(这种散列函数叫做自身函数)
    2. 数字分析法:因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。
    3. 平方取中法:取关键字平方后的中间几位作为散列地址
    4. 折叠法:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。
    5. 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p, p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。

    hash冲突及解决

    hash冲突在所难免,解决冲突是一个复杂问题。冲突主要取决于: (1)与散列函数有关,一个好的散列函数的值应尽可能平均分布。 (2)与解决冲突的哈希冲突函数有关。 (3)与负载因子的大小。太大不一定就好,而且浪费空间严重,负载因子和散列函数是联动的。 解决冲突的办法: (1)开放定址法:线性探查法、平方探查法、伪随机序列法、双哈希函数法。 (2) 链地址法:把所有同义词,即hash值相同的记录,用单链表连接起来。

    应用: 1.字符串哈希 2.加密哈希 3.几何哈希 4.布隆过滤器

    不足:获取有序序列复杂度高

    4. 二叉树搜索树

    思想

    二叉查找树(Binary Search Tree),也称有序二叉树(ordered binary tree),排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树: 1.若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 2.若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 3.任意节点的左、右子树也分别为二叉查找树。 4.没有键值相等的节点(no duplicate nodes)。 复杂度: 插入和查找的时间复杂度均为O(logN), 最坏为O(N)

    插入 :

    1. 如果当前结点是null,则创建新结点返回。

    2. 如果插入结点比当前结点值大,则插入其右孩子结点中。

    3. 如果插入结点比当前结点值小,则插入其左孩子结点中。 复杂度: 平均 O(logn) 最坏O(n) 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      public Tree insert(Tree root, int val) {
      if (root == null) {
      return new Tree(val);
      }
      if (val == root.val) {
      return root;
      } else if (val > root.val) {
      root.right = insert(root.right, val);
      } else {
      root.left = insert(root.left, val);
      }
      return root;
      }

    查找 :

    类似于二分查找,小于当前值,则去左子树查找,大于当前节点值,则去右子树查找。 时间复杂度: 平均O(logn) 最坏O(n)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public Tree search(Tree root, int val) {
    if(root == null) {
    return null;
    }
    if(root.val == val) {
    return root;
    } else if(root.val > val) {
    return search(root.left, val);
    } else {
    return search(root.right, val);
    }
    }

    查找最小值、最大值

    根据二叉搜索树的特点,最小结点都是在最左结点上,或无左子树的根节点。最大值为最右节点或根。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public Tree min(Tree root) {
    if(root == null) {
    return null;
    }
    if (root.left != null) {
    return min(root.left);
    }
    return root;
    }
    public Tree max(Tree root) {
    if(root == null) {
    return null;
    }
    if (root.right != null) {
    return max(root.right);
    }
    return root;
    }

    删除

    1. 删除最小结点

      找到根结点最左结点,如果其不存在右孩子则直接删除,否则用右孩子替换最左结点。需要考虑根结点为null和根为最小值的情况。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public Tree deleteMin(Tree root) {
      if(root == null) {
      return null;
      }
      if(root.left != null) {
      root.left = deleteMin(root.left);
      } else if (root.right != null) {
      return root.right;
      }
      return null;
      }
    2. 删除最大结点

      找到最右孩子结点,其存在左结点的话就用左结点替换否则直接删除.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public Tree deleteMax(Tree root) {
      if(root == null) {
      return null;
      }
      if(root.right != null) {
      root.right = deleteMax(root.right);
      } else if(root.left != null) {
      return root.left;
      }
      return null;
      }
    3. 删除某个结点

      首先通过查找方法找到该节点,然后删除策略:

      • 如果该节点无左右子树,直接删除

      • 如果只存在一个子树,用孩子结点替换该节点。

      • 如果存在两个子树,那么可以用左子树中最大的结点或右子树中最小结点替换,并删除子树中的节点.

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        public Tree delete(Tree root, int val) {
        if(root == null) {
        return null;
        }
        if(root.val == val) {
        if(root.left == null && root.right == null) {
        return null;
        } else if(root.left!= null && root.right != null) {
        Tree leftBig = max(root.left);
        root.val = leftBig.val;
        root.left = delete(root.left, leftBig.val);
        } else if(root.left != null){
        return root.left;
        } else {
        return root.right;
        }
        } else if(root.val < val) {
        root.right = delete(root.right, val);
        } else {
        root.left = delete(root.left, val);
        }
        return root;
        }

    5. AVL查找树

    6.1 思想

    二叉树查找树在插入时没有对二叉树的深度和结构做一个调整,使得叶子结点深度不一,在查找时深度越深的结点时间复杂度越高。为了改进查找的时间时间复杂度,于是出现了平衡二叉树(AVL).平衡二叉树使得每个结点的左结点和右结点的深度差不超过1.

    6.2 查找

    查找与二叉查找树一样。

    6.3 插入

    1
    当在AVL中插入新的结点时,需要根据实际情况对AVL中的某些结点做单旋转或双旋转操作,单旋转表示做一次顺时针或逆时针的旋转操作,而双旋转则做两次单旋转操作(先顺时针后逆时针,或者先逆时针后顺时针),单旋转发生在LL型插入和RR型插入,而双旋转则发生在LR型插入和RL型插入。以下的失去平衡点都指的是离插入点最近的那个失去平衡的结点。

    LL型:插入点位于失去平衡点的左孩子的左子树上; RR型:插入点位于失去平衡点的右孩子的右子树上; LR型:插入点位于失去平衡点的左孩子的右子树上; RR型:插入点位于失去平衡点的右孩子的左子树上。

    插入思路 和二叉搜索树的插入一样,首先在树中找到对应的位置然后插入,接着自底向上向根节点折回,于在插入期间成为不平衡的所有节点上进行旋转来完成。因为折回到根节点的路途上最多有 1.5 乘 log n 个节点,而每次AVL 旋转都耗费恒定的时间,插入处理在整体上耗费 O(log n) 时间。  具体插入过程如下:

    1. 如果当前结点为空,创建新结点返回.
    2. 如果当前结点值和插入值相同,不做处理返回。
    3. 如果插入值大于当前结点则插入到右其右孩子结点中。插入完成后比较左右孩子结点进行判断树是否失去平衡。如果是判断属于那种类型(RR, RL),并响应的旋转。最后更新当前结点的深度(孩子结点的最大深度加1,默认null深度为-1)。
    4. 否则插入到其做孩子上。 同样比较左右孩子的深度判断是否平衡,对于失去平衡的情况下做出调整。
    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
    private AvlNode<AnyType> insert(AnyType x, AvlNode<AnyType> t) {
    if (t == null)
    return new AvlNode<AnyType>(x, null, null);
    int compareResult = myCompare(x, t.element);
    if (compareResult < 0) {
    t.left = insert(x, t.left);
    if (height(t.left) - height(t.right) == 2) {
    if (myCompare(x, t.left.element) < 0) //左左情况
    t = rotateWithLeftChild(t);
    else //左右情况
    t = doubleWithLeftChild(t);
    }
    } else if (compareResult > 0) {
    t.right = insert(x, t.right);
    if (height(t.right) - height(t.left) == 2) {
    if (myCompare(x, t.right.element) < 0) //右左情况
    t = doubleWithRightChild(t);
    else //右右情况
    t = rotateWithRightChild(t);
    }
    }
    //完了之后更新height值
    t.height = Math.max(height(t.left), height(t.right)) + 1;
    return t;
    }

    6.4 删除

    从AVL树中删除可以通过把要删除的节点向下旋转成一个叶子节点,接着直接剪除这个叶子节点来完成。因为在旋转成叶子节点期间最多有 log n个节点被旋转,而每次 AVL 旋转耗费恒定的时间,删除处理在整体上耗费 O(log n) 时间。 a.当被删除节点n是叶子节点,直接删除 b.当被删除节点n只有一个孩子,删除n,用孩子替代该节点的位置 c.当被删除结点n存在左右孩子时,真正的删除点应该是n的中序遍在前驱,或者说是左子树最大的节点,之后n的值替换为真正删除点的值。这就把c归结为a,b的问题。

    从删除的结点处自低向上向根结点折回,根据当前结点的左右孩子深度判断是否平衡,如果不平衡则按选择规则进行旋转。最后更新当前结点深度,如此递归折回到根结点。

    6. B-树(B树)

    7.1 定义

    B-树是一种平衡的多路查找树,它在文件系统中很有用。 定义:一棵m 阶的B-树,或者为空树,或为满足下列特性的m 叉树: ⑴树中每个结点至多有m 棵子树; ⑵若根结点不是叶子结点,则至少有两棵子树; ⑶除根结点之外的所有非终端结点至少有⎡m/2⎤ 棵子树; ⑷所有的非终端结点中包含以下信息数据:(n,A0,K1,A1,K2,…,Kn,An) 其中:Ki(i=1,2,…,n)为关键码,且Ki< Ki+1,Ai 为指向子树根结点的指针(i=0,1,…,n),且指针Ai-1 所指子树中所有结点的关键码均小于Ki (i=1,2,…,n),An 所指子树中所有结点的关键码均大于Kn, ⎡m/2⎤ −1 ≤ n ≤m −1 ,n 为关键码的个数。 ⑸所有的叶子结点都出现在同一层次上,并且不带信息(可以看作是外部结点或查找失败的结点,实际上这些结点不存在,指向这些结点的指针为空

    7.2 查找

    B-树的查找是由两个基本操作交叉进行的过程,即 ⑴在B-树上找结点; ⑵在结点中找关键码。 由于,通常B-树是存储在外存上的,操作⑴就是通过指针在磁盘相对定位,将结点信息读入内存,之后,再对结点中的关键码有序表进行顺序查找或折半查找。因为,在磁盘上读取结点信息比在内存中进行关键码查找耗时多,每次向下搜索一层都需要从内存中加载磁盘信息,B-树的层次树是决定B-树查找效率的首要因素。

    7.2 插入

    1.插入 在B-树上插入关键码与在二叉排序树上插入结点不同,关键码的插入不是在叶结点上 进行的,而是在最底层的某个非终端结点中添加一个关键码,若该结点上关键码个数不超过m-1 个,则可直接插入到该结点上;否则,该结点上关键码个数至少达到m 个,因而使该结点的子树超过了m棵,这与B-树定义不符。所以要进行调整,即结点的“分裂”。方法为:关键码加入结点后,将结点中的关键码分成三部分,使得前后两部分关键码个数个结点将其插入到父结点中。若插入父结点而使父结点中关键码个数超过m-1,则父结点继续分裂,直到插入某个父结点,其关键码个数小于m。可见,B-树是从底向上生长的。

    7.3 删除

    分两种情况: (1)删除最底层结点中关键码 a)若结点中关键码个数大于⎡m / 2⎤ -1,直接删去。 b)否则除余项与左兄弟(无左兄弟,则找左兄弟)项数之和大于等于2( -1) 就与它 们父结点中的有关项一起重新分配 (2)删除为非底层结点中关键码 若所删除关键码非底层结点中的Ki,则可以指针Ai 所指子树中的最小关键码X 替代 Ki,然后,再删除关键码X,直到这个X 在最底层结点上,即转为(1)的情形

    7.2 B-树的特性:

    1.关键字集合分布在整颗树中; 2.任何一个关键字出现且只出现在一个结点中; 3.搜索有可能在非叶子结点结束; 4.其搜索性能等价于在关键字全集内做一次二分查找; 5.自动层次控制;

    http://c.biancheng.net/cpp/html/1028.html

    7. B+树

    8.1 定义

    ⑴有n 棵子树的结点中含有n 个关键码; ⑵所有的叶子结点中包含了全部关键码的信息,及指向含有这些关键码记录的指针,且 叶子结点本身依关键码的大小自小而大的顺序链接。 ⑶所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键码。

    8.2 B+的特性

    1.所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的; 2.不可能在非叶子结点命中; 3.非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层; 4.更适合文件索引系统;

    8.3 操作

    1)查找操作 对B+树可以进行两种查找运算: a.从最小关键字起顺序查找; b.从根结点开始,进行随机查找。 在查找时,若非终端结点上的剧组机等于给定值,并不终止,而是继续向下直到叶子结点。因此,在B+树中,不管查找成功与否,每次查找都是走了一条从根到叶子结点的路径。其余同B-树的查找类似。 2).插入操作 B+树的插入与B树的插入过程类似。不同的是B+树在叶结点上进行,如果叶结点中的关键码个数超过m,就必须分裂成关键码数目大致相同的两个结点,并保证上层结点中有这两个结点的最大关键码。(算法见百度百科) 3)删除操作 B+树的删除也仅在叶子结点进行,当叶子结点中的最大关键字被删除时,其在非终端结点中的值可以作为一个“分界关键字”存在。若因删除而使结点中关键字的个数少于m/2 (m/2结果取上界,如5/2结果为3)时,其和兄弟结点的合并过程亦和B-树类似。

    B+树和B-树最大的不同点:

    1).B-树的关键字和记录是放在一起的,叶子节点可以看作外部节点,不包含任何信息;B+树的非叶子节点中只有关键字和指向下一个节点的索引,记录只放在叶子节点中。

    2).在B-树中,越靠近根节点的记录查找时间越快,只要找到关键字即可确定记录的存在;而B+树中每个记录的查找时间基本是一样的,都需要从根节点走到叶子节点,而且在叶子节点中还要再比较关键字。从这个角度看B-树的性能好像要比B+树好,而在实际应用中却是B+树的性能要好些。因为B+树的非叶子节点不存放实际的数据,这样每个节点可容纳的元素个数比B-树多,树高比B-树小,这样带来的好处是减少磁盘访问次数。尽管B+树找到一个记录所需的比较次数要比B-树多,但是一次磁盘访问的时间相当于成百上千次内存比较的时间,因此实际中B+树的性能可能还会好些,而且B+树的叶子节点使用指针连接在一起,方便顺序遍历(例如查看一个目录下的所有文件,一个表中的所有记录等),这也是很多数据库和文件系统使用B+树的缘故。 3)B+树支持range-query非常方便,而B树不支持。这是数据库选用B+树的最主要原因http://www.cnblogs.com/biyeymyhjob/archive/2012/07/25/2608880.htmlhttp://blog.csdn.net/v_JULY_v/article/details/6530142/

    8 . 红黑树

    思想

    红黑树,一种二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。

    特性

    性质1. 节点是红色或黑色。 性质2. 根是黑色。 性质3. 所有叶子都是黑色(叶子是NIL节点)。 性质4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点) 性质5. 从任一节点到其每个叶子的所有简单路径 都包含相同数目的黑色节点。

    插入

    II、红黑树插入的几种情况: 情况1,z的叔叔y是红色的。 情况2:z的叔叔y是黑色的,且z是右孩子 情况3:z的叔叔y是黑色的,且z是左孩子

    删除

    III、红黑树删除的几种情况。 情况1:x的兄弟w是红色的。 情况2:x的兄弟w是黑色的,且w的俩个孩子都是黑色的。 情况3:x的兄弟w是黑色的,且w的左孩子是红色,w的右孩子是黑色。 情况4:x的兄弟w是黑色的,且w的右孩子是红色的。

    http://www.cnblogs.com/daoluanxiaozi/p/3340382.html

    红黑树和AVL树的比较:

    红黑树: (1)并不追求“完全平衡”——它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。红黑树能够以O(log2 n) 的时间复杂度进行搜索、插入、删除操作。 (2)此外,由于它的设计,任何不平衡都会在三次旋转之内解决。红黑树能够给我们一个比较“便宜”的解决方案。红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高。 AVL树: (1)它的左子树和右子树都是AVL树,左子树和右子树的高度差不能超过; (2) 查找、插入和删除在平均和最坏情况下都是O(log n),增加和删除可能需要通过一次或多次树旋转来重新平衡这个树; (3)一棵n个结点的AVL树的其高度保持在0(log2(n)),不会超过3/2log2(n+1) 一棵n个结点的AVL树的平均搜索长度保持在0(log2(n)). 一棵n个结点的AVL树删除一个结点做平衡化旋转所需要的时间为0(log2(n)).

    9. 哈夫曼树

    3.3 字符串

    排序

    键索引计数法:

    • 频率统计:使用int数组count[]计算每个建出现的频率
    • 将频率转化为索引:使用count来计算每个键在排序结果中的起始索引位置
    • 数据分类:将所有元素转移到一个辅助数组aux中以进行排序。
    • 回写:将元素排序后的结果复制回原数组。

    低优先字符排序

    思路:从右到左以每个位置的字符为键,用键索引计数法将字符串排序W遍(字符串长度) 复杂度: O(NW) W为字符串长度,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
    public static void sort(String[] a, int w) {
    // 通过前w个字符将a[] 排序
    int N = a.length;
    int R = 256;
    String[] aux = new String[N];
    for (int d = w - 1; d >= 0; d--) {
    int[] count = new int[R + 1];
    // 计算出频率
    for (int i = 0; i < N; i++) {
    count[a[i].charAt(d) + 1]++;
    }
    // 将频率转换为索引
    for (int r = 0; r < R; r++) {
    count[r + 1] += count[r];
    }
    // 将元素分类
    for (int i = 0; i < N; i++ ) {
    aux[count[a[i].charAt(d)] ++] = a[i];
    }
    for (int i = 0; i < N; i ++ ) {
    a[i] = aux[i];
    }
    }
    }

    高位优先字符串排序

    思路:解决字符串长度不一致的情况,采用从左到右的次序进行排序。 在高位优先排序时需要注意的字符串末尾的情况,因为字符串长度不一,合理的做法是将所有字符都已被检查的字符串所在的子数组排在所有子数组的前面,这样不需要对这些检查完成的字符串再次递归排序,也避免了IndexOutOfBound。 高位优先排序在已排号序的位置,根据字符进行分组,将数组分为更小的数组进行排序,利用了分治算法提高了算法的性能。 复杂度:O(N) 到O(Nw)之间 实现:

    三向字符串快速排序 思路:结合了快速排序和高位优先字符串排序算法,采用高位优先字符串排序算法的思路,根据字符串从左到右的字符对字符串进行排序。排序时使用快速排序的思想,使用首字符进行三向切分为首字母小余、等于,大于切分字符的字符串数组,然后递归地将得到的三个字数组排序: 优势:高位优先字符串排序算法会产生大量的子数组,而三向字符串快速排序切分总只有三个。因此该算法能够很好处理等值键、有较长公告前缀的键、取值范围娇小的键和小数组。 相对于高位优先字符串排序需要的空间更少。 复杂度: 实现:

    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
    public static void sort(String[] strArr) {
    sort(strArr, 0, strArr.length - 1, 0);
    }
    /**
    * 对字符串串数组strArr的 strArr[lo]到strArr[hi]的字符串根据d为的字符进行排序
    * @param strArr
    * @param lo
    * @param hi
    * @param d
    */
    private static void sort(String[] strArr, int lo, int hi , int d) {
    if (hi <= lo) {
    return;
    }
    int lt = lo, gt = hi;
    int val = strArr[lo].charAt(d);
    int i = lo + 1;
    while(i <= gt) {
    int tmp = strArr[i].charAt(d);
    if ( tmp < val) {
    exch(strArr, lt++, i++);
    } else if( tmp > val) {
    exch(strArr, i, gt--);
    } else {
    i ++;
    }
    }
    sort(strArr, lo, lt-1, d);
    if (val >= 0 ) {
    sort(strArr, lt, gt, d+1);
    }
    sort(strArr, gt+1, hi, d);
    }
    private static void exch(String[] a, int i ,int j) {
    String tmp = a[i];
    a[i] = a[j];
    a[j] = tmp;
    }

    注意:和快速排序一样,最好在排序之前将数组打乱或将第一个元素和一个随机位置的元素交换以得到一个随机的切分元素,避免快速排序算法接近最坏情况

    总结:

    算法 稳定性 原地排序 时间复杂度 空间复杂度 优势领域
    字符串的插入排序 N到N2之间 1 小数组或已经有序的数组
    快速排序 N(log2N) logN 通用排序算法,特别适合空间不足的情况
    归并排序 N(log2N) N 稳定的通用排序算法
    三向快速排序 N到NlogN之间 logN 大量重复建
    低位优先的字符串排序 NW N 较长的定长字符串
    高位优先的字符串排序 N到Nw之间 N+WR 随机字符串
    三向字符串快速排序 N到Nw之间 W+logN 通用排序算法,特别适合用于含有较长公共前缀的字符串

    查找树

    • 单词查找树(trie)

      基本性质: 是由连接结点组成的数据结构,除了根结点,每个结点有且只有一条从父结点指向它的链接。每条链接都对应着一个字符,也可以用链接所对应的字符标记被指向的结点。值为空的结点在符号表中没有对应的键,它们的存在是为了简化单词查找树的查找操作。 查找过程:从根结点开始,首先经过的键是首字母所对应的链接。在下一个结点中沿着第二个字符所对应的链接继续前进。依此类推,直到键的最后一个字符所指向的结点或者遇到一条空链接。此时有三种情况:

      • 键的尾字符所对应的结点中的值非空,这是一次命中查找,则返回尾字符所对应的结点中保存的值。

      • 键的尾字符所对应的结点中的值为空,表示未命中。

      • 查找结束于一条空链接,表示未命中。

        插入过程:进行插入的时候和二叉查找树一样,需要先进行查找,会遇到以下两个情况:

      • 到达键尾部之前就遇到空链接,则创建对应的结点并将键值保存到最后一个字符的结点中。

      • 在遇到空链接之前就到达了键的尾字符,在这种情况下,和关联性数组一样,将该结点的值设置为键所对应的值。

        删除过程:首先找到对应的结点并将它的值设置为空,如果该结点含有一个非空的链接指向某个子结点那么就不需要进行其他操作。如果它所有链接均为空,那么就需要从数据结构中删去这个结点,如果删去它使得它的父节点的所有链接也均为空,就需要继续删除它的父结点,依此类推。 性质:

      • 单词查找树的链表结构和键的插入或删除顺序无关:对于给定任意一组键,其单词查找树都是唯一的。

      • 在单词查找树中查找一个键或插入一个键时,访问数组的次数最多的键的长度加1.

      • 未命中查找平均所需检查的结点的数量为 logRN

      • 一棵单词查找树的链接总数在RN到RNw之间,其中w为建的平均长度。 ???

        应用:

      • 数字帐号

      • URL

      • 文本处理

      • 基因组数据中的蛋白质

        代码实现:

    • 三向单词查找树

      思想:为了避免R向单词查找树过度的浪费空间,三向单词查找树每个结点都含有一个字符、三条链接和一个值。这三条链接分别对应着当前字母小于、等于和大于结点字母的所有键。 查找:查找时比较键的首字母和根结点的字母,如果键的首字母较小,就选择左链接。如果较大,就选择右链接;如果相等,则选择中链接。然后递归的使用相同的算法,如果遇到一个空链接或则当前键结束时结点的值为空,那么查找未命中;如果键结束时结点的值非空则查找命中。 优势:节省大量空间。 性质:

      • 每个结点只有三个链接,链接总数在3N到3Nw之间

      • 平均查找时间复杂度为lnN 次

        代码实现:

    子字符串查找

    • 暴力字符串查找算法

    • KMP算法

      1
      2
      3
      4
      思想:KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,只要继续把它向后移和移动匹配词就可以,这样就提高了效率。可以针对搜索词,算出一张部分匹配表。通过查表查到最后一个匹配字符对应的部分匹配值,并利用以下公式计算匹配词向后移动的位数:
      移动位数 = 已匹配的字符数 - 对应的部分匹配值
      "部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,
      • “A”的前缀和后缀都为空集,共有元素的长度为0;
      • “AB”的前缀为[A],后缀为[B],共有元素的长度为0;
      • “ABC”的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
      • “ABCD”的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
      • “ABCDA”的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为”A”,长度为1;
      • “ABCDAB”的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为”AB”,长度为2;
      • “ABCDABD”的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

    实现:

    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
    /**
    * 计算部分匹配表
    *
    * @param pattern
    * @param next
    */
    public void makeNext(char[] pattern, int next[]) {
    int pIdx, maxSuffixLen; // pIdx:模版字符串下标;maxSuffixLen:最大前后缀长度
    int m = pattern.length; // 模版字符串长度
    next[0] = 0; //模版字符串的第一个字符的最大前后缀长度为0
    for (pIdx = 1, maxSuffixLen = 0; pIdx < m; ++pIdx) //for循环,从第二个字符开始,依次计算每一个字符对应的next值
    {
    /**
    * maxSuffixLen 大于0 表示前一个字符已经存在匹配
    */
    while (maxSuffixLen > 0 && pattern[pIdx] != pattern[maxSuffixLen]) { //递归的求出P[0]···P[q]的最大的相同的前后缀长度k
    maxSuffixLen = next[maxSuffixLen - 1]; //不理解没关系看下面的分析,这个while循环是整段代码的精髓所在,确实不好理解
    }
    if (pattern[pIdx] == pattern[maxSuffixLen]) //如果相等,那么最大相同前后缀长度加1
    {
    maxSuffixLen++;
    }
    next[pIdx] = maxSuffixLen;
    }
    }
    public int kmp(String str, String pattern) {
    int[] next = new int[str.length()];
    int strIdx, pIdx;
    makeNext(pattern.toCharArray(), next);
    for (strIdx = 0, pIdx = 0; strIdx < str.length(); ++strIdx) {
    while (pIdx > 0 && pattern.charAt(pIdx) != str.charAt(strIdx)) {
    /**
    * 移动匹配字符串位置
    */
    pIdx = next[pIdx - 1];
    }
    if (pattern.charAt(pIdx) == str.charAt(strIdx)) {
    pIdx++;
    }
    if (pIdx == pattern.length()) {
    return strIdx - pattern.length() + 1;
    }
    }
    return -1;
    }

    复杂度:时间复杂度最坏(3N) 空间复杂度 O(M)

    3.4 图

    3.5剑指offer

    01-二维数组中的查找

    在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

    思路

    坑:不要从中间去,采用二分查找。如果中间查找会有两个区域需要查找,增加复杂度。 真确思路:从右上角开始遍历,有三种情况:

    1. 如果相等,则直接返回。
    2. 如果值大于目标值,列减小。(因为从坐到右增大,因此右边的值不可能存在目标值)
    3. 如果值小于目标值,增大行。(从上往下增大,该值在最右上角为当前行最大,因此当前行没有更大的,可以往下一行找)

    思考,是否可以从左上角开始、右下角、左下角开始? 左上角不可以,右下角不可以。因为这这两个移动后还是会形成两个区域需要查找。 左下角可以。

    实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public boolean Find(int [][] array,int target) {
    if (array == null || array[0].length == 0 ) {
    return false;
    }
    int row = 0;
    int col = array.length - 1;
    while ( row < array.length && col >=0 ) {
    if ( target > array[row][col]) {
    row ++;
    } else if (target < array[row][col]) {
    col --;
    } else {
    return true;
    }
    }
    return false;
    }

    02-替换空格

    请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。

    思路

    1. 先计算空格数目
    2. 计算新的字符串长度,并构建新字符数组
    3. 逐个遍历和copy, 遇到空格用户“%20”替换

    实现

    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
    public String replaceSpace(StringBuffer str) {
    // 第一件重要的事
    if( str == null || str.length() == 0)
    return str.toString();
    // 长度计算
    int newlength = newLength(str);
    char[] newChar = new char[newlength];
    int idx = 0;
    for (int i = 0; i < str.length(); i++) {
    if (str.charAt(i) == ' ') {
    newChar[idx++] = '%';
    newChar[idx++] = '2';
    newChar[idx++] = '0';
    } else {
    newChar[idx++] = str.charAt(i);
    }
    }
    return new String(newChar);
    }
    private int newLength(StringBuffer str) {
    int spaceNum = 0;
    for (int i = 0; i < str.length(); i ++) {
    if (str.charAt(i) == ' ') {
    spaceNum++;
    }
    }
    // 或者 str.length() + spaceNum * 2
    return (str.length() - spaceNum) + spaceNum * 3;
    }

    03-从尾到头打印链表

    输入一个链表,从尾到头打印链表每个节点的值。

    思路

    利用栈,先进后出的思路。

    实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    ArrayList<Integer> result = new ArrayList<Integer>();
    if (null == listNode) {
    return result;
    }
    Stack<ListNode> stack = new Stack<ListNode>();
    ListNode next = listNode;
    while(null != next) {
    stack.push(next);
    next = next.next;
    }
    while(!stack.isEmpty()) {
    result.add(stack.pop().val);
    }
    return result;
    }

    04-旋转数组的最小数字

    把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

    思路

    二分查找,不断缩短查找范围。

    1. mid值与左右两端比较,如果小于右边,high=mid.
    2. mid大于左边,low=mid
    3. 如果左值=mid值=右值,顺序遍历
    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
    public int minNumberInRotateArray(int [] array) {
    if ( array == null || array.length == 0 ) {
    return 0;
    }
    int low = 0, high = array.length - 1;
    int mid;
    while ( low < high && array[low] >= array[high]) {
    // array[low] 需要大于等于 array[high] 在内部进行 遍历
    if ( (low +1 )== high) { //这个很关键,说明只有两个元素
    return array[high];
    }
    if (array[low] == array[high]) {
    int min = array[low];
    for ( int i = low +1; i <= high; i++) {
    if(array[i] < min){
    min = array[i];
    }
    }
    return min;
    } else {
    mid = (low + high) >> 1;
    if (array[mid] < array[high]){
    high = mid;
    } else {
    low = mid;
    }
    }
    }
    // 只有一个元素 或者 array[low] < array[high]
    return array[low];
    }

    05-斐波那契数列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public int Fibonacci(int n) {
    if(n <= 0) {
    return 0;
    }
    if (n == 1 || n == 2) {
    return 1;
    }
    int preOne = 1;
    int preTwo = 1;
    int temp;
    for ( int i = 3; i <= n; i++ ){
    temp = preOne;
    preOne = preOne + preTwo;
    preTwo = temp;
    }
    return preOne;
    }

    06-青蛙跳

    一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public int JumpFloor(int target) {
    if(target <= 0) return 0;
    if(target == 1) return 1;
    if(target == 2) return 2;
    int preOne = 2, preTwo = 1;
    for(int i = 3; i <= target; i++){
    int temp = preOne + preTwo;
    preTwo = preOne;
    preOne = temp;
    }
    return preOne;
    }

    07-变态青蛙跳

    一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public int JumpFloorII(int target) {
    if(target <= 0) return 0;
    int result = 1;
    target --;
    while(target !=0){
    result = result << 1;
    target --;
    }
    return result;
    }

    08-矩阵覆盖

    我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public int RectCover(int target) {
    if (target <= 0) return 0;
    if(target == 1) return 1;
    if(target == 2) return 2;
    int preOne = 2, preTwo =1;
    for(int i = 3; i <= target; i++){
    int temp = preOne + preTwo;
    preTwo = preOne;
    preOne = temp;
    }
    return preOne;
    }

    09-二进制中1的个数

    输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。

    思路

    如果一个整数不为0,那么这个整数至少有一位是1。如果我们把这个整数减1,那么原来处在整数最右边的1就会变为0,原来在1后面的所有的0都会变成1(如果最右边的1后面还有0的话)。其余所有位将不会受到影响。 举个例子:一个二进制数1100,从右边数起第三位是处于最右边的一个1。减去1后,第三位变成0,它后面的两位0变成了1,而前面的1保持不变,因此得到的结果是1011.我们发现减1的结果是把最右边的一个1开始的所有位都取反了。这个时候如果我们再把原来的整数和减去1之后的结果做与运算,从原来整数最右边一个1那一位开始所有位都会变成0。如1100&1011=1000.也就是说,把一个整数减去1,再和原整数做与运算,会把该整数最右边一个1变成0.那么一个整数的二进制有多少个1,就可以进行多少次这样的操作。

    实现

    1
    2
    3
    4
    5
    6
    7
    8
    public int NumberOf1(int n) {
    int count = 0;
    while(n!= 0){
    count++;
    n = n & (n - 1);
    }
    return count;
    }

    10-数值的整数次方

    给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public double Power(double base, int exponent) {
    if (base == 0) {
    return 0;
    }
    boolean flag = false; // 是否exponent是负数
    if (exponent <= 0) {
    exponent = -1 * exponent;
    flag = true;
    }
    double result = 1.0;
    while (exponent > 0) {
    result = result * base;
    exponent--;
    }
    if (flag) {
    return 1.0 / result;
    } else {
    return result;
    }
    }

    11-调整数组顺序奇数在偶数前面

    输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。

    1
    2

    二叉搜索树的后序遍历序列

    输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。

    实现

    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
    public boolean VerifySquenceOfBST(int [] sequence) {
    if (sequence == null || sequence.length ==0) {
    return false;
    }
    return verifySequenceOfBST(sequence, 0, sequence.length-1);
    }
    private boolean verifySequenceOfBST(int[] sequence,int begin,int end){
    if (begin == end) {
    return true;
    }
    int rightBegin = begin;
    int root = sequence[end];
    for (;rightBegin < end; rightBegin++ ) {
    if (sequence[rightBegin] > root ) {
    break;
    }
    }
    int i = rightBegin;
    for (; i < end; i ++ ) {
    if (sequence[i] < root) {
    return false;
    }
    }
    boolean left = (rightBegin == begin )? true : verifySequenceOfBST(sequence, begin, rightBegin -1);
    boolean right = (rightBegin == end) ? true : verifySequenceOfBST(sequence, rightBegin+1, end);
    return left & right;
    }

    复杂链表的复制

    输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)

    思路

    三步走:

    1. 在原来链中插入克隆节点,是的克隆节点和源节点相间出现。
    2. 复制random指针
    3. 从原链从剥离clone链。
    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
    public RandomListNode Clone(RandomListNode pHead)
    {
    cloneNodes(pHead);
    connectRandomNodes(pHead);
    return reconnectNodes(pHead);
    }
    private void cloneNodes(RandomListNode pHead){
    RandomListNode node = pHead;
    while(node != null){
    RandomListNode clone = new RandomListNode(node.label);
    RandomListNode temp = node.next;
    clone.next = temp;
    node.next = clone;
    node = temp;
    }
    }
    private void connectRandomNodes(RandomListNode pHead){
    RandomListNode node = pHead;
    while(node != null){
    RandomListNode clone = node.next;
    if(node.random != null)//这里一定要判断
    clone.random = node.random.next;
    node = clone.next;
    }
    }
    private RandomListNode reconnectNodes(RandomListNode pHead){
    RandomListNode node = pHead;
    RandomListNode cloneHead = null;
    RandomListNode cloneNode = null;
    if(node != null){
    cloneHead = cloneNode = node.next;
    node.next = cloneNode.next;
    node = node.next;
    }
    while(node != null){
    cloneNode.next = node.next;
    cloneNode = cloneNode.next;
    node.next = cloneNode.next;
    node = cloneNode.next;
    }
    return cloneHead;
    }

    二叉搜索树与双向链表

    输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。

    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
    public class Solution {
    public TreeNode Convert(TreeNode root){
    TreeNode lastNode = null;
    lastNode = doConvert(root, lastNode);
    while(lastNode != null && lastNode.left != null) {
    lastNode = lastNode.left;
    }
    return lastNode;
    }
    /* 转换并返回链表最后一个结点
    */
    private TreeNode doConvert(TreeNode root, TreeNode lastNode){
    if (root == null) {
    return lastNode;
    }
    if (root.left != null) {
    lastNode = doConvert(root.left, lastNode);
    }
    if(lastNode != null) {
    lastNode.right = root;
    }
    root.left = lastNode;
    lastNode = root;
    if (root.right != null) {
    lastNode = doConvert(root.right, lastNode);
    }
    return lastNode;
    }
    }

    字符串的排列

    输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。

    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
    import java.util.ArrayList;
    import java.util.Collections;
    public class Solution {
    public ArrayList<String> Permutation(String str) {
    ArrayList<String> result = new ArrayList<String>();
    if(str == null || str.length() == 0)
    return result;
    doPermutation(str.toCharArray(),0,result);
    // 实际中不需要排序
    Collections.sort(result);
    return result;
    }
    private void doPermutation(char[] array,int idx,ArrayList<String> result){
    if(idx >= array.length){
    String str = new String(array);
    result.add(str);
    return;
    }
    for(int i=idx; i < array.length; i++){ //i得从 idx开始
    if(i != idx && array[i] == array[idx] )
    continue;
    swap(array,idx,i);
    doPermutation(array,idx+1,result);
    swap(array,idx,i);
    }
    }
    private void swap(char[] array,int i,int j){
    char temp = array[i];
    array[i] = array[j];
    array[j] = temp;
    }
    }

    3.6 Leetcode


    第四章 JAVA基础

    4.1 j2se基础

    • 值传递和引用传递

      java本质上还是值传递,即传入一个变量的副本。如果这个变量是一个对象引用,则方法栈内会创建一个副本指向相同的引用对象,此时对该对象的内容操作会改变该对象,但是无法修改对象引用的值。

      1
      2
      3
      4
      5
      6
      public void change(Item m){
      m.name = "new name"; // m是引用对象n的一个副本,指向n的内存,因此n的值发生变化
      m = new Item(); // 更改副本m的引用指向新的内存,传入对象n指向的内存地址不改变
      }
      Item n = new Item("name");
      change(n);
    • 静态变量和实例变量的区别

      静态变量前要加 static 关键字,静态变量属于类,只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,直接使用类名来访问。实例变量属于某个对象的属性,必须创建了实例对象,其中的实例变量才会被分配空间,才能使用。

    • JDK 常见的package

      • java.lang:这个是系统的基础类,比如 String 等都是这里面的,唯一一个可以不用 import 就可以使用。
      • java.io: 这里面是所有输入输出有关的类,比如文件操作等
      • java.net: 这里面是与网络有关的类,比如 URL,URLConnection 等。
      • java.util: 这个是系统辅助类,特别是集合类 Collection,List,Map 等。
      • java.sql: 这个是数据库操作的类,Connection, Statememt,ResultSet 等
    • JDK, JRE 和 JVM 的区别

      JDK, JRE 和 JVM 的区别 JDK 可以编译调试运行java程序, JRE 只能运行 Java 程序。 JDK 和 JRE 都包含了 JVM,JVM是跨平台的。 即时编译器(JIT) 是 JVM 的一部分,Java编译器将代码编译成JVM可识别的字节码,而不是机器二进制指令,然后由JIT将字节码转换成与平台相关的机器码指令。

    • final, finally, finalize 的区别?

      final:(1)修饰类:表示该类不能被继承; (2)修饰方法:表示方法不能被覆盖; (3)修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。

      finally:通常放在 try…catch 的后面构造总是执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码都能执行,可以将释放外部资源的代码写在 finally 块中。

      finalize:Object 类中定义的方法finalize(),用于对象被GC回收前执行一些清理工作。GC在销毁对象时调用,通过重写finalize() 方法可以实现整理系统资源或者执行其他清理工作。

    • assert关键字

      assertion(断言)在软件开发中是一种常用的调试方式,assertion 用于保证程序最基本、关键的正确性。assertion 检查通常在开发和测试时开启。为了提高性能,在软件发布后, assertion 检查通常是关闭的。在实现中,断言是一个包含布尔表达式的语句,在执行这个语句时假定该表达式为 true;如果表达式计算为 false,那么系统会报告一个 AssertionError。

      1
      2
      assert Expression1;
      assert Expression1 : Expression2 ; // 表达式2用于输出相关的调试信息

      断言在默认情况下是禁用的,要在编译时启用断言,需使用 source 1.4 标记:javac -source 1.4 Test.java

      要在运行时启用断言,可使用 -enableassertions 或者 -ea 标记。 要在运行时选择禁用断言,可使用 -da 或者 -disableassertions 标记。

    • char 型变量

      char 类型可以存储一个中文汉字,因为 Java 中使用的编码是 Unicode,一个 char 类型占 2 个字节(16bit),所以放一个中文是没问题的。在 JVM 内部都是 Unicode,当这个字符被从 JVM 内部转移到外部时(例如存入文件系统中),需要进行编码转换,因此Java 中提供了字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如 InputStreamReader 和 OutputStreamReader.

    • String 和StringBuilder、StringBuffer 的区别

      String是只读字符串,不可改变对象。 StringBuffer 和 StringBuilder 类表示的字符串对象可以直接进行修改。StringBuilder 是线程不安全的,因此它的效率也比 StringBuffer 略高。

    • 不可变对象

      不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。

      • 将类声明为final,所以它不能被继承
      • 将所有的成员声明为私有的,这样就不允许直接访问这些成员,将所有可变的成员声明为final,这样只能对它们赋值一次
      • 通过构造器初始化所有成员,进行深拷贝(deep copy):如果某一个类成员不是原始变量(primitive)或者不可变类,必须通过在成员初始化(in)或者get方法(out)时通过深度clone方法,来确保类的不可变。
      • 在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝
    • 为什么String要设计成不可变的

      1. 字符串常量池的需要,节约内存空间。
      2. 线程安全考虑。 同一个字符串实例可以被多个线程共享。
      3. 类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。
      4. 支持hash映射和缓存。

      Javac 编译可以对字符串常量直接相加的表达式进行优化,不必要等到运行期去进行加法运算处理,而是在编译时去掉其中的加号,直接将其编译成一个这些常量相连的结果。所以 String s=“a”+”b”+”c”+”d”;只生成一个对象.

    • Java 序列化

      序列化就是一种用来处理对象流的机制,用于处理对象流的读写。对象流可以在网络中进行对象传输。

      序列化的实现:将需要被序列化的类实现 Serializable 接口,该接口没有需要实现的方法, implements Serializable 只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来写流对象,恢复的话则用输入流。

    • Java持久化

      通常将对象由内存保存到硬盘上,使其在程序生命周期之外也可以使用。必须提供Save, Load,boolExist(判断外存是否已有该对象)。

    • 错误和异常(Error vs Exception)

      1. java.lang.Error ,表示系统级的错误和程序不必处理的异常,如内存溢出。java.lang.Exception ,表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题;二者都是Throwable的子类。
      2. Error 和 RuntimeException 及其子类都是未检查的异常(unchecked exceptions),而所有其他的 Exception 类都是检查了的异常(checked exceptions) 与上下文环境有关,即使程序设计无误,仍然可能因使用的问题而引发.发生在编译阶段,必须要使用 try…catch(或者 throws )否则编译不通过。 unchecked exceptions发生在运行期,具有不确定性,主要是由于程序的逻辑问题所引起的。比如 类型转换、数组越界访问和试图访问空指针等等。
    • try{} 里的 return 语句

      Java 允许在 finally 中改变返回值,因为try 中的 return 语句立即执行,而是记录下返回点,等 finally 代码块执行完毕之后再执行return语句。如果在 finally 中修改了返回值,return的结果也会有相应的改变。

    • throws、throw、try、catch、finally

      try 用来运行一段可能“异常”的代码,catch 子句用来指定想要捕捉的“异常”的类型,并处理;finally,保证无论是否发生“异常”,都会执行finally代码块;throw 语句可以在程序的任意位置主动抛出一个“异常”对象;throws用来标明一个成员函数可能抛出的各种“异常”;

    • 常见的runtime exception

      1、NullPointerException空指针引用异常 2、ClassCastException- 类型强制转换异常。 3、IllegalArgumentException- 传递非法参数异常。 4、IndexOutOfBoundsException- 下标越界异常 5、UnsupportedOperationException - 不支持的操作异常 6、ArithmeticException - 算术运算异常 7、ArrayStoreException - 向数组中存放与声明类型不兼容对象异常 8、NegativeArraySizeException - 创建一个大小为负数的数组错误异常 9、NumberFormatException - 数字格式异常 10、SecurityException - 安全异常

    • equals与==的区别

      • == 是一个运算符,可以用于基本类型的值比较,对象比较则是判断两个引用是否是同一对象。
      • equals则是Object提供的对象比较方法,用于判断两个对象引用是否为同一对象(对象大小的比较,是实现Comparable接口)。重写equals方法需要注意:
        1. 使用 instanceof 操作符检查“参数是否为正确的类型”
        2. 对于类中的关键属性,检查传入对象的属性是否与之相匹配;
        3. 编写完 equals 方法后,问自己它是否满足对称性、传递性、一致性;
        4. 重写 equals 时总是要重写 hashCode
        5. 不要将 equals 方法参数中的 Object 对象替换为其他的类型,在重写时不要忘掉 @Override 注解。
    • Object对象提供的方法

      1. protected Object clone()创建并返回此对象的一个副本。
      2. public boolean equals(Object obj)指示其他某个对象是否与此对象“相等”。
      3. protected void finalize()当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
      4. public final native Class< ? > getClass() 返回此 Object 的运行时类。
      5. public int hashCode()返回该对象的哈希码值。
      6. public String toString()返回该对象的字符串表示。
      7. public void notify()唤醒在此对象监视器上等待的单个线程。 public void notifyAll()唤醒在此对象监视器上等待的所有线程。
      8. public void wait()在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,阻塞当前线程等待。

      注意: 对象必须实现Cloneable标示性接口,才能调用Clone方法,否则会有CloneNotSupportedException 异常。

      在每个覆盖equals方法的类中也必须覆盖hashCode方法,由于hashCode的通用约定—相等的对象散列码相等,可以保证所有基于散列集合的操作运行正常,如HashMap,HashSet和HashTable。hashMap根据hashCode去定位散列桶位置。

    • 修饰符顺序

      public protected private abstract static final transient volatile synchronized native strictfp

    4.2 面向对象

    面向对象软件开发的优点: (1) 代码开发模块化,更易维护和修改。 (2) 代码复用。 (3) 增强代码的可靠性和灵活性。 (4) 增加代码的可理解性。 面向对象编程有很多重要的特性如:封装,继承,多态和抽象。

    • Overload 和 Override

      重载 Overload 表示同一个类中可以有多个名称相同的方法,但这些方法的参数类型、数量不同。 不能通过访问权限、返回类型、抛出异常实现重载。

      重写 Override 表示子类中的方法可以与父类中的某个方法的名称和参数完全相同,通过子类创建的实例对象调用这个方法时,将调用子类中的定义方法,这相当于把父类中定义的那个完全相同的方法给覆盖了,这也是面向对象编程的多态性的一种表现。子类覆盖父类的方法时,只能比父类抛出更少的异常,或者是抛出父类抛出的异常的子异常,因为子类可以解决父类的一些问题,不能比父类有更多的问题。子类方法的访问权限只能比父类的更大,不能更小。如果父类的方法是 private 类型,那么,子类则不存在覆盖的限制,相当于子类中增加了一个全新的方法。

    • 接口和抽象类的区别

      1. 接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。
      2. 类可以实现很多个接口,但是只能继承一个抽象类
      3. 类如果要实现一个接口,它必须要实现接口声明的所有方法。类在声明成是抽象的情况下,可以不实现抽象类声明的所有方法。
      4. Java 接口中声明的变量默认都是 final 的.抽象类可以包含非 final 的变量。
      5. Java 接口中的成员函数默认是 public 的。抽象类的成员函数可以是 private,protected 或者是 public 。
      6. 接口是绝对抽象的,不可以被实例化。抽象类也无法实例化,但是可以调用main方法。
    • 接口

      1. 接口的变量都是 public static final ,相当于常量,是所有实现类的共有变量。 方法都是public abstract
      2. 没有构造函数和实例变量,没法被实例化。
    • AOP 和 OOP,IOC 和 DI

      1. 面向对象编程(Object Oriented Programming,OOP,面向对象程序设计)是一种计算机编程架构。

      2. AOP 是 OOP 的延续,是 Aspect Oriented Programming 的缩写,意思是面向方面编程。 将通用需求功能从不相关类之中分离出来;同时,能够使得很多类共享一个行为,一旦行为发生变化,不必修改很多类,只要修改这个行为就可以。AOP 就是这种实现分散关注的编程方法,它将“关注”封装在“方面”中。

        类比Python的装饰器语法,可以在函数运行时,改变函数的运行。

      3. 控制反转 IOC(Inversion of Control) 控制指的就是程序相关类之间的依赖关系.传统观念设计中, 通常由调用者来创建被调用者的实例, 在 Spring 里,创建被调用者的工作不再由调用者来完成,而是由 Spring 容器完成,依赖关系被反转了,称为控制反转,目的是为了获得更好的扩展性和良好的可维护性。依赖注入(Dependency injection)创建被调用者的工作由 Spring 容器完成,然后注入调用者,因此也称依赖注入。控制反转和依赖注入是同一个概念

    4.3 集合

    Collection:List列表,Set集合, Map:Hashtable,HashMap,TreeMap

    • List 列表,元素有序,可重复,能够使用索引访问元素。List接口的常用实现类: Vector,线程安全但速度慢,底层数据结构是数组;ArrayList,线程不安全但查询速度快,底层数据结构是数组; LinkedList,线程不安全,增删速度快,底层数据结构是链表。

    • Set(集) 元素不可重复,无序。 取出元素的方法只有迭代器。 Set接口中常用的类 HashSet:线程不安全,内部是基于散列函数实现,存取速度快。依赖元素的hashCode方法和euqals方法确保唯一性; LinkedHashSet,具有HashSet的查询速度,且内部采用双向链表维护元素的顺序 ,内部是由链表实现;TreeSet,线程不安全,可以对Set集合中的元素进行排序,内部采用了红黑树。通过元素的compareTo或者compare方法来保证元素的唯一性。元素是以二叉树的形式存放的。TreeSet中的元素要实现Comparable接口,或者有一个自定义的比较器。

    • Map,key-value的形式保存索引和数据。Hashtable,线程安全,速度快。底层是哈希表数据结构,不允许null作为key和值;Properties:用于配置文件的定义和操作,使用频率非常高,同时键和值都是字符串;HashMap,线程不安全,速度慢。底层也是哈希表数据结构,但是允许null作为键和值;LinkedHashMap,可以保证HashMap集合有序。存入的顺序和取出的顺序一致;TreeMap,可以对Map中的键进行排序,底层是采用红黑树.

    • Collection 和 Collections的区别: Collection是集合类的上级接口,子接口主要有Set 和List、Map。 Collections是针对集合类的实现的工具类,提供了操作集合的工具方法:一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。

    • ArrayList 和 LinkedList

      底层实现不同,ArrayList使用数组,查询快;LinkedList使用链表,插入删除速度快;

    • 遍历List

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      List<String> strList = new ArrayList<>();
      //使用for-each循环
      for(String obj : strList){
      System.out.println(obj);
      }
      //using iterator
      Iterator<String> it = strList.iterator();
      while(it.hasNext()){
      String obj = it.next();
      System.out.println(obj);
      }
    • LinkedList 和PriorityQueue 的区别

      Queue中只有两个实现类LinkedList 和PriorityQueue。他们都是属于队列,即拥有先进先出(FIFO)的特点。LinkedList 支持双向列表操作, PriorityQueue 按优先权控制队列元素的出队次序(Comparable接口)。

    • HashMap实现原理

      数据结构: HashMap基于哈希算法,采用链地址法来避免hash冲突,所以其内部采用链表数组数据结构。我们通过put()和get()方法储存和获取对象。HashMap里面实现一个静态内部类Entry, Entry用于保存key、value, next为链表指针.

      存取实现: put ,根据对象的hashCode计算得到散列桶坐标(数组下表),由于数组中存放的是Entry链表结构,然后遍历Entry链表,如果存在key则更新其Value,如果不存在则采用头插入的方式在链表头部插入一个新的Entry节点存储key-value。 null key总是存放在Entry[]数组的第一个元素。

      get, 根据hashCode的值定位到散列桶,并遍历存放在改桶中的链表。如果存在的该key则返回对应的value,如果不存在则返回null。

      解决hash冲突:1.开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列) 2.再哈希法 3.链地址法 4.建立一个公共溢出区。 Java的HashMap使用的是链地址法。

    • Enumeration 接口和 Iterator 接口的区别

      Enumeration 速度是 Iterator 的2倍,同时占用更少的内存。 但是,Iterator远远比 Enumeration 安全,因为其他线程不能够修改正在被 iterator 遍历的集合里面的对象。 同时,Iterator 允许调用者删除底层集合里面的元素。

    • 迭代器fail-fast属性

      每次尝试获取下一个元素的时候,Iterator fail-fast属性检查当前集合结构里的任何改动。如果发现任何改动,它抛出ConcurrentModificationException。Collection中所有Iterator的实现都是按fail-fast来设计。在遍历一个集合的时候,我们可以使用并发集合类来避免ConcurrentModificationException,比如使用CopyOnWriteArrayList,而不是ArrayList

    • 快速失败(fail-fast)和安全失败(fail-safe)的区别

      Iterator的fail-fast属性与当前的集合共同起作用,因此它不会受到集合中任何改动的影响。java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。

    • Map接口提供的集合视图

      1. Set keyset():返回map中包含的所有key的一个Set视图。
      2. Collection values():返回一个map中包含的所有value的一个Collection视图。
      3. Set< Map.Entry < K,V > > entrySet():返回一个map钟包含的所有映射的一个集合视图。

      注意: 上面三个方法得到的集合对象是Map里的引用对象,都不支持add添加元素,但是支持移除和修改,相应的Map对象元素内容也会发生改变。

    • EnumSet

      java.util.EnumSet是使用枚举类型的集合实现。当集合创建时,枚举集合中的所有元素必须来自单个指定的枚举类型,可以是显示的或隐示的。

    • 集合对象排序

      可以使用Arrays.sort()或Collections.sort()方法。

    • Comparable和Comparator接口

      都是用来实现集合中元素的比较、排序的。只是 Comparable 用于实现对象自比较。Comparator 实现两个对象的比较。

      Comparator里有compare(T o1,T o2)和equals(Object obj),Comparable接口只有int compareTo(T o)方法。

    4.4 多线程

    并发编程常见的3个问题:原子性,可见性(共享变量修改对其他线程立即可用),有序性。

    • volatile的理解

      1. Volatile 是轻量级的synchronized,它在多处理器开发过程中保证了共享变量的“可见性”,可见性是指当一个线程的某个共享变量发生改变时,另一个线程能够读取到这个修改的值。
    1. Volatile 可以禁止指令重排序。
    2. Volatile 能保持单个简单volatile变量的读/写操作的具有原子性。但不能保证自增自减的原子性。
    • 死锁的必要条件?怎么克服?

      答:产生死锁的四个必要条件: 互斥条件:一个资源每次只能被一个进程使用。 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 不剥夺条件:进程已获得的资源,在末使用完之前,不能被强行剥夺。 循环等待条件。 只要上述条件之一不满足,就不会发生死锁。

    • ThreadPool

      创建和销毁线程需要代价,线程执行完毕不销毁而是存入线程池中。

      1. execute 和submit的区别

        Execute()用于提交不需要返回值得任务,submit()用于提交需要返回值的任务,发挥Future类型的对象。

      2. Shutdown和shutdownNow的区别

        都是遍历线程池中的工作线程,然后逐个调用线程的Internet方法来中断线程,所以无法响应中断的任务可能永远无法终止。 ShutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有正在执行或暂停的任务,并返回等待执行任务的列表。而shutdown只是将线程池设置成SHUTDOWN状态,然后中断没有正在执行任务的线程。

    • ThreadLocal

      ThreadLocal并不是一个Thread,而是Thread的局部变量,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,一个线程不会影响其它线程所对应的变量副本

    • 同步

      1. sleep() 和 wait() 区别

        sleep()是Tread的方法,可以让线程暂停执行指定时间,将执行机会给其他线程,但是依然保持监控状态,到时后线程变为ready状态,因此调用 sleep后线程 不会释放对象锁。wait() 是 Object 类的方法,调用 wait()方法导致本线程放弃对象锁(线程暂停执行),然后等待针对此对象发出 notify 方法(或 notifyAll),本线程才能获得对象锁,进入就绪状态。

      2. sleep() 和 yield() 区别

        sleep方法给其他线程运行机会时不考虑线程的优先级,因此低优先级的线程也有运行的机会;yield方法只会给相同优先级或更高优先级的线程以运行的机会;

        sleep() 方法后线程转入阻塞(blocked)状态,而执行 yield方法后线程转入就绪(ready)状态;

        sleep() 方法声明需要抛出InterruptedException,而 yield() 方法没有声明任何异常;

    • 实现多线程的3种方法:Thread与Runable。 1)继承Tread类,重写run函数 2)实现Runnable接口 3)实现Callable接口

      • Callable和Runnable的区别

    4.5 IO

    4.6 设计模式

    设计模式:单例、工厂、适配器、责任链、观察者等等。

    4.7 JVM

    根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。

    mark

    • 程序计数器:看做当前线程所执行的字节码行号指示器。是线程私有的内存,且唯一一块不报OutOfMemoryError异常。
    • Java虚拟机栈:用于描述java方法的内存模型:每个方法被执行时都会同时创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机中从入栈到出栈的过程。如果线程请求的栈深度大于虚拟机所允许的深度就报StackOverflowError, 如果虚拟机栈可以动态扩展,当拓展时无法申请到足够的内存会抛出OutOfMemoryError. 是线程私有的。
    • 本地方法栈:与虚拟机栈相似,不同的在于它是为虚拟机使用到的Native方法服务的。会抛出StackOverflowError和OutOfMemoryError。是线程私有的。
    • Java堆:是所有线程共享的一块内存,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。如果堆上没有内存完成实例的分配就会报OutOfMemoryError.
    • 方法区(永久代):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。当方法区无法满足内存分配需求时,会抛出OutOfMemoryError。是共享内存。
    • 运行时常量池:用于存放编译器生成的各种字面量和符号引用,是方法区的一部分。无法申请内存时抛出OutOfMemoryError。
    • 直接内存:不是虚拟机运行时数据的一部分,也不是java虚拟机规范中定义的区域,是计算机直接的内存空间。这部分也被频繁使用,如JAVA NIO的引入基于通道和缓存区的I/O使用native函数直接分配堆外内存。如果内存不足会报OutOfMemoryError。

    GC的两种判定方法:引用计数与根搜索算法。

    • 引用计数: 给对象添加一个引用计数器,每当有一个地方引用该对象时,计数器值加1,当引用失效时,计数器值减1。任何时候计数器都为0的对象就是不可能再被使用的。它很难解决对象之间相互循环引用问题。
    • 根搜索算法(GC Roots Traceing): 通过一系列名为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用。 GC Roots对象一般是:虚拟机栈中的引用对象,方法区中类静态属性引用的对象,方法区常量引用的对象等。

    Java中的四种对象引用,可以通过代码决定某些对象的生命周期,同时利于JVM进行垃圾回收。

    1. 强引用:程序代码中的普通引用。如Object obj = new Object(),只要强引用存在,垃圾回收器就不会回收。在不使用对象时应及时将引用设置为null,便于垃圾回收。
    2. 软引用:描述一些有用但并非必须的对象。对于软引用关联的对象在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。
    3. 弱引用:描述非必须对象,比软引用弱一些。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
    4. 虚引用:最弱的引用,不管是否有虚引用存在,完全不会对对象生存时间构成影响,也无法通过虚引用来取得一个对象实例。唯一目的是希望能够在这个对象被垃圾回收器之前收到系统通知。

    内存溢出和内存泄漏

    1. 内存溢出:通俗理解就是内存不够,程序所需要的内存远远超出了你虚拟机分配的内存大小
    2. 内存泄露:动态分配的内存空间使用完毕之后未回收

    内存溢出了怎么办?

    ​ 通过内存映像工具如jhat,jconsole等对dump出来的堆转存储快照进行分析,确认内存是出现内存泄露还是内存溢出。

    • 如果是内存泄露进一步使用工具查看泄露的对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们。掌握泄露对象的信息,以及GC Roots引用链的信息,就可以比较准确定位泄露代码的位置。
    • 如果是内存溢出,那就需要通过jinfo,Jconsole等工具分析java堆参数与机器物理内存对比是否还可以调大,从代码上检查是否存在某些对象生命周期过长,持有状态过长的情况,尝试减少程序的运行消耗。

    Java 中有内存泄露吗?

    1. 长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,例如缓存系统中,加载了一个对象放在缓存中,然后一直不再使用它,造成内存泄露。
    2. 如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,也不会被垃圾回收,造成内存泄露。

    GC的三种收集方法

    1. 标记清理:首先标记所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记的对象。缺点是效率低,且存在内存碎片。主要用于老生代垃圾回收。
    2. 复制算法:将内存按容量划分为大小相等的一块,每次只用其中一块。当内存用完了,将还存活的对象复制到另一块内存,然后把已使用过的内存空间一次清理掉。实现简单,高效。一般用于新生代。一般是将内存分为一块较大的Eden空间和两块较小的Survivor空间。HotSpot虚拟机默认比例是8:1,。每次使用Eden和一块Survivor,当回收时将这两块内存中还存活的对象复制到Survivor然后清理掉刚才Eden和Survivor的空间。如果复制过程内存不够使用则向老年代分配担保。
    3. 标记整理:首先标记所有需要回收的对象,在标记完成后让所有存活的对象都向一端移动,然后直接清理掉端边界意外的内存。用于老年代。

    JVM 加载 class 文件的原理机制

    • 类加载的五个过程:加载、验证、准备、解析、初始化。
    • 类加载器把类的 .class 文件中的数据读入到内存中,然后进入连接阶段,这一阶段包括验证(确保class文件对JVM合法)、准备(为静态变量分配内存并设置默认的初始值)和解析(将常量池的符号引用替换为直接引用)三个步骤。最后 JVM 对类进行初始化,包括:初始化直接父类,执行类中的构造方法。

    Student s= new Student(),在内存中做了那些事情

    1. 加载Student.class 文件进内存
    2. 在栈内存为s开辟空间
    3. 在堆内存为Student对象开辟空间
    4. 学生对象的成员变量进行显示初始化
    5. 通过构造方法对学生对象变量赋值
    6. 学生对象初始完毕,把对象地址赋值给s变量

    JVM常用参数配置

    • -Xms :表示初始堆大小,默认为物理内存的1/64(<1GB),默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.
    • -Xmx :最大堆大小,物理内存的1/4(<1GB),(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
    • -Xmn :年轻代大小(1.4or lator),注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的整个堆大小=年轻代大小 + 年老代大小 + 持久代大小.增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8
    • -XX:NewSize :设置年轻代大小(for 1.3/1.4)
    • -XX:MaxNewSize :年轻代最大值(for 1.3/1.4)
    • -XX:PermSize 设置持久代(perm gen)初始值,物理内存的1/64
    • -XX:MaxPermSize :设置持久代最大值,物理内存的1/4

    第五章 数据库

    存储引擎的区别

    InnoDB: 支持事务,是面向在线事务处理(OLTP)的应用,特点是行锁设计,支持外键,并支持一致性非锁定读,即默认情况下读取操作不会产生锁.是默认的存储引擎:.还提供了插入缓冲,二次写,自适应哈希索引,预读等高性能和高可用的功能.

    MyISAM: 不支持事务,是表锁设计和支持全文索引,主要面向一些OLAP的数据库应用.它的缓冲池只缓冲索引文件,而不缓冲数据文件.该存储引擎表由MYD和MYI组成,MYD用来存放数据文件,MYI用来存放索引文件.

    NDB:是一个集群存储引擎,其特点是数据全部放在内存中,因此主键查找速度极快,并通过添加NDB数据库存储节点可以线性提高数据库性能,是高可用,高性能的集群系统.

    Memory: 将表中的数据存放在内存中,如果数据库重启或发生崩溃,表中的数据库都将消失,它非常适合存储临时数据的临时表.默认采用哈希索引.

    Archive: 只支持INSERT和SELECT操作,使用zlib算法将数据行进行压缩,压缩比可以达到1:10,非常适合存储归档数据.但其本身不是事务安全的存储引擎,其设计目标是提供高速的插入和压缩功能.

    Federated: 并不存放数据,它只是指向一台远程MySQL数据库服务器上的表.

    Maria存储引擎: 设计目标主要是用来取代原有的MyISAM存储引擎.

    MyISAM和InnoDB的区别

    1. MyISAM是非事务安全型的,而InnoDB是事务安全型的。
    2. MyISAM锁的粒度是表级,而InnoDB支持行级锁定。
    3. MyISAM支持全文索引,而Innodb不支持全文索引
    4. MyISAM表是保存成文件形式的,在跨平台的数据转移中使用MyISAM存储会省去不少的麻烦。
    5. InnoDB表比MyISAM表更安全,可以保证数据不丢失的情况下,切换非事务表到事务表

    sql注入原理

    • 在用户传递的参数中插入SQL命令,最终达到欺骗服务器执行恶意的SQL命令。
    • 防注入:限制字符长度,对特殊字符转义。 后台不要使用动态拼装SQL,可以使用参数化的SQL或者直接使用存储过程进行数据查询存取。 不要使用root权限建立数据库连接。重要信息加密存储。 数据库异常信息不应暴露给用户。

    数据库索引

    索引是一个单独存储在磁盘上的数据库结构,它们包含着对数据表里所有记录的引用指针,使用索引可以提高数据库特定数据的查询速度.索引时在存储引擎中实现的,因此每种存储引擎的索引不一定完全相同,并且每种存储引擎也不一定支持所有索引类型.

    索引的存储类型有两种:BTREE和HASH,具体和表的存储引擎有关.MyISAM和InnoDB存储引擎只支持BTREE;MEMORY/HEAD存储索引可以支持HASH和BTREE索引. 索引的优点:

    1. 通过创建唯一索引,可以保证数据库表中每行数据的唯一性.
    2. 可以加快数据的查询速度.
    3. 在实现数据的参考完整性方面,可以加速表和表之间的连接.
    4. 再使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间
    5. 通过使用索引,可以在查询中使用优化隐藏器,提高系统的性能。

    索引的缺点

    1. 创建索引和维护索引要耗费时间,并且随着数据量的增加耗费时间也增加.
    2. 索引需要占空间内存.
    3. 在对表中数据进行增加,删除和修改的时候,索引也需要动态维护,这样降低了数据维护速度.

    索引分类

    1. 普通索引和唯一索引
    2. 直接创建索引和间接创建索引
    3. 普通索引和唯一性索引
    4. 单个索引和符合索引
    5. 聚簇索引和非聚簇索引

    索引失效??

    1. WHERE字句的查询条件里有不等于号(WHERE column!=…),MYSQL将无法使用索引
    2. 如果WHERE字句的查询条件里使用了函数(如:WHERE DAY(column)=…),MYSQL将无法使用索引
    3. 在JOIN操作中(需要从多个数据表提取数据时),MYSQL只有在主键和外键的数据类型相同时才能使用索引,否则即使建立了索引也不会使用。
    4. 如果WHERE子句的查询条件里使用了比较操作符LIKE和REGEXP,MYSQL只有在搜索模板的第一个字符不是通配符的情况下才能使用索引。比如说,如果查询条件是LIKE ‘abc%’,MYSQL将使用索引;如果条件是LIKE ‘%abc’,MYSQL将不使用索引。
    5. 在ORDER BY操作中,MYSQL只有在排序条件不是一个查询条件表达式的情况下才使用索引。尽管如此,在涉及多个数据表的查询里,即使有索引可用,那些索引在加快ORDER BY操作方面也没什么作用。
    6. 如果某个数据列里包含着许多重复的值,就算为它建立了索引也不会有很好的效果。比如说,如果某个数据列里包含了净是些诸如“0/1”或“Y/N”等值,就没有必要为它创建一个索引。
    7. 如果条件中有or(并且其中有or的条件是不带索引的),即使其中有条件带索引也不会使用(这也是为什么尽量少用or的原因)。注意:要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引。
    8. 如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引。
    9. 如果mysql估计使用全表扫描要比使用索引快,则不使用索引。

    数据库锁机制

    数据库锁定机制简单来说就是数据库为了保证数据的一致性而使各种共享资源在被并发访问,访问变得有序所设计的一种规则。MySQL各存储引擎使用了三种类型(级别)的锁定机制:行级锁定,页级锁定和表级锁定。MySQL数据库中,使用表级锁定的主要是MyISAM,Memory,CSV等一些非事务性存储引擎,而使用行级锁定的主要是Innodb存储引擎和NDBCluster存储引擎,页级锁定主要是BerkeleyDB存储引擎的锁定方式。

    • 表级锁定(table-level):表级别的锁定是MySQL各存储引擎中最大颗粒度的锁定机制。该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。当然,锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也会最高,致使并大度大打折扣。表级锁分为读锁和写锁。
    • 页级锁定(page-level):页级锁定的特点是锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁。
    • 行级锁定(row-level):行级锁定最大的特点就是锁定对象的颗粒度很小,也是目前各大数据库管理软件所实现的锁定颗粒度最小的。由于锁定颗粒度很小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能。虽然能够在并发处理能力上面有较大的优势,但是行级锁定也因此带来了不少弊端。由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行级锁定也最容易发生死锁。InnoDB的行级锁同样分为两种,共享锁和排他锁,同样InnoDB也引入了意向锁(表级锁)的概念,所以也就有了意向共享锁和意向排他锁,所以InnoDB实际上有四种锁,即共享锁(S)、排他锁(X)、意向共享锁(IS)、意向排他锁(IX);
    • MyISAM 表锁优化建议: 1、缩短锁定时间 2、分离能并行的操作 3、合理利用读写优先级

    悲观锁:它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制。悲观的缺陷是不论是页锁还是行锁,加锁的时间可能会很长,这样可能会长时间的限制其他用户的访问,也就是说悲观锁的并发访问性不好。

    乐观锁( Optimistic Locking ) :相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则则拒绝更新并返回用户错误的信息,让用户决定如何去做。乐观锁由程序实现,不会存在死锁问题。它适用的场景也相对乐观。但乐观锁不能解决脏读的问题

    悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。 乐观锁不能解决脏读的问题。

    数据库连接池原理

    背景

    数据库连接的创建和关闭需要一定的开销,频繁的建立、关闭数据库,增大系统开销,另外还必须管理每一个连接,以确保他们能正确关闭,否则会导致内存溢出。

    原理

    数据库连接池的基本思想就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。以及一套连接使用、分配、管理策略,使得该连接池中的连接可以得到高效、安全的复用,避免了数据库连接频繁建立、关闭的开销。我


    第六章 框架和组件

    6.1 spring

    Spring优点

    • 轻量级:Spring在大小和透明性方面绝对属于轻量级的,基础版本的Spring框架大约只有2MB。
    • 控制反转(IOC):Spring使用控制反转技术实现了松耦合。依赖被注入到对象,而不是创建或寻找依赖对象。
    • 面向切面编程(AOP): Spring支持面向切面编程,同时把应用的业务逻辑与系统的服务分离开来
    • 容器:Spring包含并管理应用程序对象的配置及生命周期。
    • MVC框架:Spring的web框架是一个设计优良的web MVC框架,很好的取代了一些web框架。
    • 事务管理:Spring对下至本地业务上至全局业务(JAT)提供了统一的事务管理接口。
    • 异常处理:Spring提供一个方便的API将特定技术的异常(由JDBC, Hibernate, 或JDO抛出)转化为一致的、Unchecked异常。

    Spring框架模块

    核心容器模块:是spring中最核心的模块。负责Bean的创建,配置和管理。主要包括:beans, core, context, expression等模块。 AOP模块:主要负责对面向切面编程的支持,帮助应用对象解耦。 数据访问和集成模块:包括JDBC,ORM,OXM,JMS和事务处理模块。Web和远程调用:包括web,servlet,struts,portlet模块。 测试模块, 工具模块 消息模块

    • 什么是控制反转(IOC)?什么是依赖注入?

      传统模式中对象的调用者需要创建被调用对象,两个对象过于耦合,不利于变化和拓展.在spring中,直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理,从而实现对象之间的松耦合。所谓的“控制反转”概念就是对组件对象控制权的转移,从程序代码本身转移到了外部容器。 依赖注入:对象无需自行创建或管理它们的依赖关系,IoC容器在运行期间,动态地将某种依赖关系注入到对象之中。依赖注入能让相互协作的软件组件保持松散耦合。

    • BeanFactory和ApplicationContext有什么区别?

      Bean工厂(BeanFactory)是Spring框架最核心的接口,提供了高级Ioc的配置机制.应用上下文(ApplicationContext)建立在BeanFacotry基础之上,提供了更多面向应用的功能,如果国际化,属性编辑器,事件等等.beanFactory是spring框架的基础设施,是面向spring本身,ApplicationContext是面向使用Spring框架的开发者,几乎所有场合都会用到ApplicationContext.

    • Spring有几种配置方式?

      1. 基于XML的配置
      2. 基于注解的配置,可以用注解的方式来替代XML方式对bean描述,可以将bean描述转移到组件类的内部,只需要在相关类上、方法上或者字段声明上使用注解即可。
      3. 基于Java的配置:由@Configuration注解和@Bean注解来实现的。由@Bean注解的方法将会实例化、配置和初始化一个新对象,这个对象将由Spring的IoC容器来管理。@Bean声明所起到的作用与 元素类似。被@Configuration所注解的类则表示这个类的主要目的是作为bean定义的资源。被@Configuration声明的类可以通过在同一个类的内部调用@bean方法来设置嵌入bean的依赖关系。
    • Spring Bean的作用域之间有什么区别

      singleton:这种bean范围是默认的,这种范围确保不管接受到多少个请求,每个容器中只有一个bean的实例,单例的模式由bean factory自身来维护。

      prototype:原形范围与单例范围相反,为每一个bean请求提供一个实例。

      request:在请求bean范围内会每一个来自客户端的网络请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收。

      Session:与请求范围类似,确保每个session中有一个bean的实例,在session过期后,bean会随之失效。 global-session:global-session和Portlet应用相关。当你的应用部署在Portlet容器中工作时,它包含很多portlet。如果你想要声明让所有的portlet共用全局的存储变量的话,那么这全局变量需要存储在global-session中。

    • Spring 框架中都用到了哪些设计模式

      1. 代理模式—在AOP和remoting中被用的比较多。
      2. 单例模式—在spring配置文件中定义的bean默认为单例模式。
      3. 模板方法—用来解决代码重复的问题 比如. RestTemplate, JmsTemplate, JpaTemplate。 前端控制器—Srping提供了DispatcherServlet来对请求进行分发。 视图帮助(View Helper )—Spring提供了一系列的JSP标签,高效宏来辅助将分散的代码整合在视图里。 依赖注入—贯穿于BeanFactory / ApplicationContext 接口的核心理念。
      4. 工厂模式—BeanFactory用来创建对象的实例。
      5. Builder模式 - 自定义配置文件的解析bean是时采用builder模式,一步一步地构建一个beanDefinition
      6. 策略模式 :Spring 中策略模式使用有多个地方,如 Bean 定义对象的创建以及代理对象的创建等。这里主要看一下代理对象创建的策略模式的实现。
    • AOP是怎么实现的

      实现AOP的技术,主要分为两大类:

      1. 动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;
      2. 静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。
    • springMVC流程具体叙述下
      1. 应用启动时,容器会加载servlet类并调用init方法. 在这个阶段,DispatcherServlet在init()完成初始化参数init-param的解析和封装,相关配置,spring的WebApplicationContext的初始化即完成xml文件的加载,bean的解析和注册等工作,另外为servlet功能所用的变量进行初始化,如:handlerMapping,viewResolvers等.
      2. 收到一个请求时,首先根据请求的类型调用DispatcherServlet不同的方法,这些方法都会转发到doService()中执行.根据request信息获取对应的Handler. 首先根据request获取访问路径,然后根据该路径URL匹配对应规则的Handler,调用Handler处理。
    • ioc注入的方式

    1)setter方法注入 2)构造器注入3)静态工厂注入 factory-method参数 4)实例工厂 <>

    • AOP相关概念
      1. 方面(Aspect):一个关注点的模块化,这个关注点实现可能另外横切多个对象。事务管理是J2EE应用中一个很好的横切关注点例子。方面用Spring的 Advisor或拦截器实现。
      2. 连接点(Joinpoint): 程序执行过程中明确的点,如方法的调用或特定的异常被抛出。
      3. 通知(Advice): 在特定的连接点,AOP框架执行的动作。各种类型的通知包括“around”、“before”和“throws”通知。许多AOP框架包括Spring都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器链。
      4. 切入点(Pointcut): 一系列连接点的集合。
      5. 引入(Introduction): 添加方法或字段到被通知的类。 Spring允许引入新的接口到任何被通知的对象。例如,你可以使用一个引入使任何对象实现 IsModified接口,来简化缓存。Spring中要使用Introduction, 可有通过DelegatingIntroductionInterceptor来实现通知,通过DefaultIntroductionAdvisor 来配置Advice和代理类要实现的接口
      6. 目标对象(Target Object): 包含连接点的对象。也被称作被通知或被代理对象。POJO
      7. AOP代理(AOP Proxy): AOP框架创建的对象,包含通知。 在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
      8. 织入(Weaving): 组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。
    • 过滤器与监听器的区别

      过滤器Filter,是一个变种servlet,无法直接返回给用户response,但是可以链式处理request、response。

    • Spring Beans

      Spring Beans是构成Spring应用核心的Java对象。这些对象由Spring IOC容器实例化、组装、管理。这些对象通过容器中配置的元数据创建,例如,使用XML文件中定义的创建。 在Spring中创建的beans都是单例的beans。在bean标签中有一个属性为”singleton”,如果设为true,该bean是单例的,如果设为false,该bean是原型bean。Singleton属性默认设置为true。因此,spring框架中所有的bean都默认为单例bean。

    • Spring Bean中定义了什么内容?

      Spring Bean中定义了所有的配置元数据,这些配置信息告知容器如何创建它,它的生命周期是什么以及它的依赖关系。

    6.2 spring-MVC

    6.3 Mybatis

    第七章 面试

    7.1 语言基础

    7.2 算法

    7.3 项目

    Python开发复习资料

    1. Python语言特性
      1. 1 Python的函数参数传递
      2. 2 Python中的元类(metaclass)
      3. 3 @staticmethod和@classmethod
      4. 4 类变量和实例变量
      5. 5 Python自省
      6. 6 字典推导式
      7. 7 Python中单下划线和双下划线
      8. 8 字符串格式化:%和.format
      9. 9 迭代器和生成器
      10. 10 *args and **kwargs
      11. 11 面向切面编程AOP和装饰器
      12. 12 鸭子类型
      13. 13 Python中重载
      14. 14 新式类和旧式类
      15. 15 __new__和__init__的区别
      16. 16 单例模式
        1. 1 使用__new__方法
        2. 2 共享属性
        3. 3 装饰器版本
        4. 4 import方法
      17. 17 Python中的作用域
      18. 18 GIL线程全局锁
      19. 19 协程
      20. 20 闭包
      21. 21 lambda函数
      22. 22 Python函数式编程
      23. 23 Python里的拷贝
      24. 24 Python垃圾回收机制
        1. 1 引用计数
        2. 2 标记-清除机制
        3. 3 分代技术
      25. 25 Python的List
      26. 26 Python的is
      27. 27 read,readline和readlines
      28. 28 Python2和3的区别
      29. 29 super init
      30. 30 range and xrange
    2. 操作系统
      1. 1 select,poll和epoll
      2. 2 调度算法
      3. 3 死锁
      4. 4 程序编译与链接
        1. 1 预处理
        2. 2 编译
        3. 3 汇编
        4. 4 链接
      5. 5 静态链接和动态链接
      6. 6 虚拟内存技术
      7. 7 分页和分段
        1. 分页与分段的主要区别
      8. 8 页面置换算法
      9. 9 边沿触发和水平触发
    3. 数据库
      1. 1 事务
      2. 2 数据库索引
      3. 3 Redis原理
        1. Redis是什么?
        2. Redis数据库
        3. Redis缺点
        4. Redis应用
          1. 数据缓存
          2. 消息队列
          3. 时间轴
          4. 排行榜
          5. 计数器
          6. 好友关系
          7. 分布式锁
          8. 倒排索引
      4. 4 乐观锁和悲观锁
      5. 5 MVCC
      6. 6 MyISAM和InnoDB
    4. 网络
      1. 1 三次握手
      2. 2 四次挥手
      3. 3 ARP协议
      4. 4 urllib和urllib2的区别
      5. 5 Post和Get
      6. 6 Cookie和Session
      7. 7 apache和nginx的区别
      8. 8 网站用户密码保存
      9. 9 HTTP和HTTPS
      10. 10 XSRF和XSS
      11. 11 幂等 Idempotence
      12. 12 RESTful架构(SOAP,RPC)
      13. 13 SOAP
      14. 14 RPC
      15. 15 CGI和WSGI
      16. 16 中间人攻击
      17. 17 c10k问题
      18. 18 socket
      19. 19 浏览器缓存
      20. 20 HTTP1.0和HTTP1.1
      21. 21 Ajax
    5. UNIX
      1. unix进程间通信方式(IPC)
    6. 数据结构
      1. 1 红黑树
    7. 编程题
      1. 1 台阶问题/斐波那契
      2. 2 变态台阶问题
      3. 3 矩形覆盖
      4. 4 杨氏矩阵查找
      5. 5 去除列表中的重复元素
      6. 6 链表成对调换
      7. 7 创建字典的方法
        1. 1 直接创建
        2. 2 工厂方法
        3. 3 fromkeys()方法
      8. 8 合并两个有序列表
      9. 9 交叉链表求交点
      10. 10 二分查找
      11. 11 快排
      12. 12 找零问题
      13. 13 广度遍历和深度遍历二叉树
      14. 14 二叉树节点
      15. 15 层次遍历
      16. 16 深度遍历
      17. 17 前中后序遍历
      18. 18 求最大树深
      19. 19 求两棵树是否相同
      20. 20 前序中序求后序
      21. 21 单链表逆置
      22. 22 两个字符串是否是变位词
      23. 23 动态规划问题
        1. 01背包问题
        2. 完全背包问题
        3. 多重背包问题
    8. Python Web后端面试
      1. Git Flow工作流程
        1. 主要分支
        2. 开发流程
        3. 发布后Bug修复
        4. 撤销操作
      2. Job1
      3. Job2
      4. Job3
      5. Job4
      6. Job5

    Table of Contents

    Python语言特性

    1 Python的函数参数传递

    看两个例子:

    1
    2
    3
    4
    5
    a = 1
    def fun(a):
    a = 2
    fun(a)
    print a # 1
    1
    2
    3
    4
    5
    a = []
    def fun(a):
    a.append(1)
    fun(a)
    print a # [1]

    所有的变量都可以理解是内存中一个对象的“引用”,或者,也可以看似c中void*的感觉。

    通过id来看引用a的内存地址可以比较理解:

    1
    2
    3
    4
    5
    6
    7
    8
    a = 1
    def fun(a):
    print "func_in",id(a) # func_in 41322472
    a = 2
    print "re-point",id(a), id(2) # re-point 41322448 41322448
    print "func_out",id(a), id(1) # func_out 41322472 41322472
    fun(a)
    print a # 1

    注:具体的值在不同电脑上运行时可能不同。

    可以看到,在执行完a = 2之后,a引用中保存的值,即内存地址发生变化,由原来1对象的所在的地址变成了2这个实体对象的内存地址。

    而第2个例子a引用保存的内存值就不会发生变化:

    1
    2
    3
    4
    5
    6
    7
    a = []
    def fun(a):
    print "func_in",id(a) # func_in 53629256
    a.append(1)
    print "func_out",id(a) # func_out 53629256
    fun(a)
    print a # [1]

    这里记住的是类型是属于对象的,而不是变量。而对象有两种,“可更改”(mutable)与“不可更改”(immutable)对象。在python中,strings, tuples, 和numbers是不可更改的对象,而 list, dict, set 等则是可以修改的对象。(这就是这个问题的重点)

    当一个引用传递给函数的时候,函数自动复制一份引用,这个函数里的引用和外边的引用没有半毛关系了.所以第一个例子里函数把引用指向了一个不可变对象,当函数返回的时候,外面的引用没半毛感觉.而第二个例子就不一样了,函数内的引用指向的是可变对象,对它的操作就和定位了指针地址一样,在内存里进行修改.

    2 Python中的元类(metaclass)

    这个非常的不常用,但是像ORM这种复杂的结构还是会需要的,类是元类的实例,元类可以定义类的行为。

    3 @staticmethod和@classmethod

    Python其实有3个方法,即静态方法(staticmethod),类方法(classmethod)和实例方法,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    def foo(x):
    print "executing foo(%s)"%(x)
    class A(object):
    def foo(self,x):
    print "executing foo(%s,%s)"%(self,x)
    @classmethod
    def class_foo(cls,x):
    print "executing class_foo(%s,%s)"%(cls,x)
    @staticmethod
    def static_foo(x):
    print "executing static_foo(%s)"%x
    a=A()

    这里先理解下函数参数里面的self和cls.这个self和cls是对类或者实例的绑定,对于一般的函数来说我们可以这么调用foo(x),这个函数就是最常用的,它的工作跟任何东西(类,实例)无关.对于实例方法,我们知道在类里每次定义方法的时候都需要绑定这个实例,就是foo(self, x),为什么要这么做呢?因为实例方法的调用离不开实例,我们需要把实例自己传给函数,调用的时候是这样的a.foo(x)(其实是foo(a, x)).类方法一样,只不过它传递的是类而不是实例,A.class_foo(x).注意这里的self和cls可以替换别的参数,但是python的约定是这俩,还是不要改的好.

    对于静态方法其实和普通的方法一样,不需要对谁进行绑定,唯一的区别是调用的时候需要使用a.static_foo(x)或者A.static_foo(x)来调用.

    \ 实例方法 类方法 静态方法
    a = A() a.foo(x) a.class_foo(x) a.static_foo(x)
    A 不可用 A.class_foo(x) A.static_foo(x)

    4 类变量和实例变量

    类变量:

    ​ 是可在类的所有实例之间共享的值(也就是说,它们不是单独分配给每个实例的)。例如下例中,num_of_instance 就是类变量,用于跟踪存在着多少个Test 的实例。

    实例变量:

    实例化之后,每个实例单独拥有的变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Test(object):
    num_of_instance = 0
    def __init__(self, name):
    self.name = name
    Test.num_of_instance += 1
    if __name__ == '__main__':
    print Test.num_of_instance # 0
    t1 = Test('jack')
    print Test.num_of_instance # 1
    t2 = Test('lucy')
    print t1.name , t1.num_of_instance # jack 2
    print t2.name , t2.num_of_instance # lucy 2

    补充的例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Person:
    name="aaa"
    p1=Person()
    p2=Person()
    p1.name="bbb"
    print p1.name # bbb
    print p2.name # aaa
    print Person.name # aaa

    这里p1.name="bbb"是实例调用了类变量,这其实和上面第一个问题一样,就是函数传参的问题,p1.name一开始是指向的类变量name="aaa",但是在实例的作用域里把类变量的引用改变了,就变成了一个实例变量,self.name不再引用Person的类变量name了.

    可以看看下面的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Person:
    name=[]
    p1=Person()
    p2=Person()
    p1.name.append(1)
    print p1.name # [1]
    print p2.name # [1]
    print Person.name # [1]

    5 Python自省

    这个也是python彪悍的特性.

    自省就是面向对象的语言所写的程序在运行时,所能知道对象的类型.简单一句就是运行时能够获得对象的类型.比如type(),dir(),getattr(),hasattr(),isinstance().

    1
    2
    3
    4
    5
    a = [1,2,3]
    b = {'a':1,'b':2,'c':3}
    c = True
    print type(a),type(b),type(c) # <type 'list'> <type 'dict'> <type 'bool'>
    print isinstance(a,list) # True

    6 字典推导式

    可能你见过列表推导时,却没有见过字典推导式,在2.7中才加入的:

    1
    d = {key: value for (key, value) in iterable}

    7 Python中单下划线和双下划线

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    >>> class MyClass():
    ... def __init__(self):
    ... self.__superprivate = "Hello"
    ... self._semiprivate = ", world!"
    ...
    >>> mc = MyClass()
    >>> print mc.__superprivate
    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    AttributeError: myClass instance has no attribute '__superprivate'
    >>> print mc._semiprivate
    , world!
    >>> print mc.__dict__
    {'_MyClass__superprivate': 'Hello', '_semiprivate': ', world!'}

    __foo__:一种约定,Python内部的名字,用来区别其他用户自定义的命名,以防冲突,就是例如__init__(),__del__(),__call__()这些特殊方法

    _foo:一种约定,用来指定变量私有.程序员用来指定私有变量的一种方式.不能用from module import * 导入,其他方面和公有一样访问;

    __foo:这个有真正的意义:解析器用_classname__foo来代替这个名字,以区别和其他类相同的命名,它无法直接像公有成员一样随便访问,通过对象名._类名__xxx这样的方式可以访问.

    8 字符串格式化:%和.format

    .format在许多方面看起来更便利.对于%最烦人的是它无法同时传递一个变量和元组.你可能会想下面的代码不会有什么问题:

    1
    "hi there %s" % name

    但是,如果name恰好是(1,2,3),它将会抛出一个TypeError异常.为了保证它总是正确的,你必须这样做:

    1
    2
    3
    "hi there %s" % (name,) # 提供一个单元素的数组而不是一个参数
    "hi there {1} {2} {0}".format("str0", "str1", "str2")
    "hi there {name} ".format(name=name,)

    但是有点丑..format就没有这些问题。format的参数可以指定顺序或命名.

    你为什么不用它?

    • 不知道它(在读这个之前)
    • 为了和Python2.5兼容(譬如logging库建议使用%(issue #4))

    9 迭代器和生成器

    这里有个关于生成器的创建问题面试官有考:
    问: 将列表生成式中[]改成() 之后数据结构是否改变?
    答案:是,从列表变为生成器

    1
    2
    3
    4
    5
    6
    >>> L = [x*x for x in range(10)]
    >>> L
    [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    >>> g = (x*x for x in range(10))
    >>> g
    <generator object <genexpr> at 0x0000028F8B774200>

    通过列表生成式,可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含百万元素的列表,不仅是占用很大的内存空间,如:我们只需要访问前面的几个元素,后面大部分元素所占的空间都是浪费的。因此,没有必要创建完整的列表(节省大量内存空间)。在Python中,我们可以采用生成器:边循环,边计算的机制—>generator

    10 *args and **kwargs

    *args**kwargs只是为了方便并没有强制使用它们.

    当你不确定你的函数里将要传递多少参数时你可以用*args.例如,它可以传递任意数量的参数:

    1
    2
    3
    4
    5
    6
    7
    8
    >>> def print_everything(*args):
    for count, thing in enumerate(args):
    ... print '{0}. {1}'.format(count, thing)
    ...
    >>> print_everything('apple', 'banana', 'cabbage')
    0. apple
    1. banana
    2. cabbage

    相似的,**kwargs允许你使用没有事先定义的参数名:

    1
    2
    3
    >>> def table_things(**kwargs):
    ... for name, value in kwargs.items():
    ... print '{0} = {1}'.format(name, value)

    你也可以混着用.命名参数首先获得参数值然后所有的其他参数都传递给*args**kwargs.命名参数在列表的最前端.例如:

    1
    def table_things(titlestring, **kwargs)

    *args**kwargs可以同时在函数的定义中,但是*args必须在**kwargs前面.

    当调用函数时你也可以用***语法.例如:

    1
    2
    3
    4
    5
    >>> def print_three_things(a, b, c):
    ... print 'a = {0}, b = {1}, c = {2}'.format(a,b,c)
    ...
    >>> mylist = ['aardvark', 'baboon', 'cat']
    >>> print_three_things(*mylist)

    就像你看到的一样,它可以传递列表(或者元组)的每一项并把它们解包.注意必须与它们在函数里的参数相吻合.当然,你也可以在函数定义或者函数调用时用*.

    11 面向切面编程AOP和装饰器

    装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的有插入日志、性能测试、事务处理等。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

    12 鸭子类型

    “当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”

    我们并不关心对象是什么类型,到底是不是鸭子,只关心行为。

    比如在python中,有很多file-like的东西,比如StringIO,GzipFile,socket。它们有很多相同的方法,我们把它们当作文件使用。

    又比如list.extend()方法中,我们并不关心它的参数是不是list,只要它是可迭代的,所以它的参数可以是list/tuple/dict/字符串/生成器等.

    鸭子类型在动态语言中经常使用,非常灵活,使得python不想java那样专门去弄一大堆的设计模式。

    13 Python中重载

    函数重载主要是为了解决两个问题。

    1. 可变参数类型。
    2. 可变参数个数。

    另外,一个基本的设计原则是,仅仅当两个函数除了参数类型和参数个数不同以外,其功能是完全相同的,此时才使用函数重载,如果两个函数的功能其实不同,那么不应当使用重载,而应当使用一个名字不同的函数。

    好吧,那么对于情况 1 ,函数功能相同,但是参数类型不同,python 如何处理?答案是根本不需要处理,因为 python 可以接受任何类型的参数,如果函数的功能相同,那么不同的参数类型在 python 中很可能是相同的代码,没有必要做成两个不同函数。

    那么对于情况 2 ,函数功能相同,但参数个数不同,python 如何处理?大家知道,答案就是缺省参数。对那些缺少的参数设定为缺省参数即可解决问题。因为你假设函数功能相同,那么那些缺少的参数终归是需要用的。

    因此python 自然就不需要函数重载

    14 新式类和旧式类

    这篇文章很好的介绍了新式类的特性: http://www.cnblogs.com/btchenguang/archive/2012/09/17/2689146.html

    新式类很早在2.2就出现了,所以旧式类完全是兼容的问题,Python3里的类全部都是新式类.这里有一个MRO问题可以了解下(新式类是广度优先,旧式类是深度优先),Python核心编程里讲的也很多.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # 新式类, 继承自Object
    class New(Object):
    pass
    # 旧类
    class A():
    def foo1(self):
    print "A"
    class B(A):
    def foo2(self):
    pass
    class C(A):
    def foo1(self):
    print "C"
    class D(B, C):
    pass
    d = D()
    d.foo1()
    # A

    按照经典类的查找顺序从左到右深度优先的规则,在访问d.foo1()的时候,D这个类是没有的..那么往上查找,先找到B,里面没有,深度优先,访问A,找到了foo1(),所以这时候调用的是A的foo1(),从而导致C重写的foo1()被绕过

    15 __new____init__的区别

    这个__new__确实很少见到,先做了解吧.

    1. __new__是一个静态方法,而__init__是一个实例方法.
    2. __new__方法会返回一个创建的实例,而__init__什么都不返回.
    3. 只有在__new__返回一个cls的实例时后面的__init__才能被调用.
    4. 当创建一个新实例时调用__new__,初始化一个实例时用__init__.

    ps: __metaclass__是创建类时起作用.所以我们可以分别使用__metaclass__,__new____init__来分别在类创建,实例创建和实例初始化的时候做一些小手脚.

    16 单例模式

    ​ 单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。

    __new__()__init__()之前被调用,用于生成实例对象。利用这个方法和类的属性的特点可以实现设计模式的单例模式。单例模式是指创建唯一对象,单例模式设计的类只能实例
    这个绝对常考啊.绝对要记住1~2个方法,当时面试官是让手写的.

    1 使用__new__方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Singleton(object):
    def __new__(cls, *args, **kw):
    if not hasattr(cls, '_instance'):
    orig = super(Singleton, cls) # 获取cls的父类, super做为代理对象,帮助我们调用其第一个参数的MRO链上的下一个类的方法。
    cls._instance = orig.__new__(cls, *args, **kw) # 创建一个实例
    return cls._instance
    class MyClass(Singleton):
    a = 1

    2 共享属性

    创建实例时把所有实例的__dict__指向同一个字典,这样它们具有相同的属性和方法.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Borg(object):
    _state = {}
    def __new__(cls, *args, **kw):
    ob = super(Borg, cls).__new__(cls, *args, **kw)
    ob.__dict__ = cls._state
    return ob
    class MyClass2(Borg):
    a = 1

    3 装饰器版本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    def singleton(cls, *args, **kw):
    instances = {}
    def getinstance():
    if cls not in instances:
    instances[cls] = cls(*args, **kw)
    return instances[cls]
    return getinstance
    @singleton
    class MyClass:
    pass

    4 import方法

    作为python的模块是天然的单例模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # mysingleton.py
    class My_Singleton(object):
    def foo(self):
    pass
    my_singleton = My_Singleton()
    # to use
    from mysingleton import my_singleton
    my_singleton.foo()

    17 Python中的作用域

    Python 中,一个变量的作用域总是由在代码中被赋值的地方所决定的。

    当 Python 遇到一个变量的话他会按照这样的顺序进行搜索:

    本地作用域(Local)→当前作用域被嵌入的本地作用域(Enclosing locals)→全局/模块作用域(Global)→内置作用域(Built-in)

    18 GIL线程全局锁

    线程全局锁(Global Interpreter Lock),即Python为了保证线程安全而采取的独立线程运行的限制,说白了就是一个核只能在同一时间运行一个线程.对于io密集型任务,python的多线程起到作用,但对于cpu密集型任务,python的多线程几乎占不到任何优势,还有可能因为争夺资源而变慢。

    解决办法就是多进程和下面的协程(协程也只是单CPU,但是能减小切换代价提升性能).

    19 协程

    简单点说协程是进程和线程的升级版,进程和线程都面临着内核态和用户态的切换问题而耗费许多切换时间,而协程就是用户自己控制切换的时机,不再需要陷入系统的内核态.

    协程是自己放弃运行状态转入就绪状态,让权给优先级更高的协程,低优先级的无法获得运行权。

    线程切换是被剥夺运行,进入阻塞状态,然后调度新的线程运行。

    20 闭包

    闭包(closure)是函数式编程的重要的语法结构。闭包也是一种组织代码的结构,它同样提高了代码的可重复使用性。

    当一个内嵌函数引用其外部作作用域的变量,我们就会得到一个闭包. 总结一下,创建一个闭包必须满足以下几点:

    1. 必须有一个内嵌函数
    2. 内嵌函数必须引用外部函数中的变量
    3. 外部函数的返回值必须是内嵌函数

    感觉闭包还是有难度的,几句话是说不明白的,还是查查相关资料.

    重点是函数运行后并不会被撤销,就像16题的instance字典一样,当函数运行完后,instance并不被销毁,而是继续留在内存空间里.这个功能类似类里的类变量,只不过迁移到了函数上.

    闭包就像个空心球一样,你知道外面和里面,但你不知道中间是什么样.

    21 lambda函数

    其实就是一个匿名函数,为什么叫lambda?因为和函数式编程有关.

    22 Python函数式编程

    python中函数式编程支持:

    filter 函数的功能相当于过滤器。调用一个布尔函数bool_func来迭代遍历每个seq中的元素;返回一个使bool_seq返回值为true的元素的序列。

    1
    2
    3
    4
    >>>a = [1,2,3,4,5,6,7]
    >>>b = filter(lambda x: x > 5, a)
    >>>print b
    >>>[6,7]

    map函数是对一个序列的每个项依次执行函数,下面是对一个序列每个项都乘以2:

    1
    2
    3
    >>> a = map(lambda x:x*2,[1,2,3])
    >>> list(a)
    [2, 4, 6]

    reduce函数是对一个序列的每个项迭代调用函数,下面是求3的阶乘:

    1
    2
    >>> reduce(lambda x,y:x*y,range(1,4))
    6

    23 Python里的拷贝

    引用和copy(),deepcopy()的区别

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import copy
    a = [1, 2, 3, 4, ['a', 'b']] #原始对象
    b = a #赋值,传对象的引用
    c = copy.copy(a) #对象拷贝,浅拷贝
    d = copy.deepcopy(a) #对象拷贝,深拷贝
    a.append(5) #修改对象a
    a[4].append('c') #修改对象a中的['a', 'b']数组对象
    print 'a = ', a
    print 'b = ', b
    print 'c = ', c
    print 'd = ', d
    输出结果:
    a = [1, 2, 3, 4, ['a', 'b', 'c'], 5]
    b = [1, 2, 3, 4, ['a', 'b', 'c'], 5]
    c = [1, 2, 3, 4, ['a', 'b', 'c']]
    d = [1, 2, 3, 4, ['a', 'b']]

    24 Python垃圾回收机制

    Python GC主要使用引用计数(reference counting)来跟踪和回收垃圾。在引用计数的基础上,通过“标记-清除”(mark and sweep)解决容器对象可能产生的循环引用问题,通过“分代回收”(generation collection)以空间换时间的方法提高垃圾回收效率。

    1 引用计数

    PyObject是每个对象必有的内容,其中ob_refcnt就是做为引用计数。当一个对象有新的引用时,它的ob_refcnt就会增加,当引用它的对象被删除,它的ob_refcnt就会减少.引用计数为0时,该对象生命就结束了。

    优点:

    1. 简单
    2. 实时性

    缺点:

    1. 维护引用计数消耗资源
    2. 循环引用

    2 标记-清除机制

    基本思路是先按需分配,等到没有空闲内存的时候从寄存器和程序栈上的引用出发,遍历以对象为节点、以引用为边构成的图,把所有可以访问到的对象打上标记,然后清扫一遍内存空间,把所有没标记的对象释放。

    3 分代技术

    分代回收的整体思想是:将系统中的所有内存块根据其存活时间划分为不同的集合,每个集合就成为一个“代”,垃圾收集频率随着“代”的存活时间的增大而减小,存活时间通常利用经过几次垃圾回收来度量。

    Python默认定义了三代对象集合,索引数越大,对象存活时间越长。

    举例:
    当某些内存块M经过了3次垃圾收集的清洗之后还存活时,我们就将内存块M划到一个集合A中去,而新分配的内存都划分到集合B中去。当垃圾收集开始工作时,大多数情况都只对集合B进行垃圾回收,而对集合A进行垃圾回收要隔相当长一段时间后才进行,这就使得垃圾收集机制需要处理的内存少了,效率自然就提高了。在这个过程中,集合B中的某些内存块由于存活时间长而会被转移到集合A中,当然,集合A中实际上也存在一些垃圾,这些垃圾的回收会因为这种分代的机制而被延迟。

    25 Python的List

    使用的数组结构,因此存在重新分配内存的问题。

    26 Python的is

    is是对比地址,==是对比值

    27 read,readline和readlines

    • read 读取整个文件
    • readline 读取下一行,使用生成器方法
    • readlines 读取整个文件到一个迭代器以供我们遍历

    28 Python2和3的区别

    • py2可以使用__future__导入支持py3新特性的包
    • print变为函数
    • 整除, py3 中/是带小数的除, "//"是整除
    • py3有 unicode字符串,以及字节类byte
    • py3 中没有xrange,range即为生成器实现

    29 super init

    MRO: Method Resolution Order

    super() lets you avoid referring to the base class explicitly, which can be nice. But the main advantage comes with multiple inheritance, where all sorts of fun stuff can happen. See the standard docs on super if you haven’t already.

    Note that the syntax changed in Python 3.0: you can just say super().__init__() instead of super(ChildB, self).__init__() which MRO is quite a bit nicer.

    30 range and xrange

    ​ xrange生成器模式,占用内存小

    操作系统

    1 select,poll和epoll

    IO多路复用机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

    但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

    • select有3个缺点:连接数受限(1024),查找配对速度慢,数据由内核拷贝到用户态。
    • poll改善了select第一个缺点,但是查找时遍历全部文件句柄;epoll改了三个缺点,查找匹配时仅返回状态发生变化的文件句柄。

    不仅仅是文件IO,还有网络IO

    epoll技术的编程模型就是异步非阻塞回调,也可以叫做Reactor,事件驱动,事件轮循(EventLoop)。Nginx,libevent,node.js这些就是Epoll时代的产物。tornado也使用了epoll.

    2 调度算法

    1. 先来先服务(FCFS, First Come First Serve)
    2. 短作业优先(SJF, Shortest Job First)
    3. 最高优先权调度(Priority Scheduling)
    4. 时间片轮转(RR, Round Robin)
    5. 多级反馈队列调度(multilevel feedback queue scheduling)

    实时调度算法:

    1. 最早截至时间优先 EDF
    2. 最低松弛度优先 LLF

    3 死锁

    原因:

    1. 竞争资源
    2. 程序推进顺序不当

    必要条件:

    1. 互斥条件
    2. 请求和保持条件
    3. 不剥夺条件
    4. 环路等待条件

    处理死锁基本方法:

    1. 预防死锁(摒弃除1以外的条件)
    2. 避免死锁(银行家算法)
    3. 检测死锁(资源分配图)
    4. 解除死锁
      1. 剥夺资源
      2. 撤销进程

    4 程序编译与链接

    Bulid过程可以分解为4个步骤:预处理(Prepressing), 编译(Compilation)、汇编(Assembly)、链接(Linking)

    以c语言为例:

    1 预处理

    预编译过程主要处理那些源文件中的以“#”开始的预编译指令,主要处理规则有:

    1. 将所有的“#define”删除,并展开所用的宏定义
    2. 处理所有条件预编译指令,比如“#if”、“#ifdef”、 “#elif”、“#endif”
    3. 处理“#include”预编译指令,将被包含的文件插入到该编译指令的位置,注:此过程是递归进行的
    4. 删除所有注释
    5. 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息以及用于编译时产生编译错误或警告时可显示行号
    6. 保留所有的#pragma编译器指令。

    2 编译

    编译过程就是把预处理完的文件进行一系列的词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件。这个过程是整个程序构建的核心部分。

    3 汇编

    汇编器是将汇编代码转化成机器可以执行的指令,每一条汇编语句几乎都是一条机器指令。经过编译、链接、汇编输出的文件成为目标文件(Object File)

    4 链接

    链接的主要内容就是把各个模块之间相互引用的部分处理好,使各个模块可以正确的拼接。
    链接的主要过程包块 地址和空间的分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)等步骤。

    5 静态链接和动态链接

    静态链接方法:静态链接的时候,载入代码就会把程序会用到的动态代码或动态代码的地址确定下来
    静态库的链接可以使用静态链接,动态链接库也可以使用这种方法链接导入库

    动态链接方法:使用这种方式的程序并不在一开始就完成动态链接,而是直到真正调用动态库代码时,载入程序才计算(被调用的那部分)动态代码的逻辑地址,然后等到某个时候,程序又需要调用另外某块动态代码时,载入程序又去计算这部分代码的逻辑地址,所以,这种方式使程序初始化时间较短,但运行期间的性能比不上静态链接的程序

    6 虚拟内存技术

    虚拟存储器是指具有请求调入功能和置换功能,能从逻辑上对内存容量加以扩充的一种存储系统.

    7 分页和分段

    分页: 用户程序的地址空间被划分成若干固定大小的区域,称为“页”,相应地,内存空间分成若干个物理块,页和块的大小相等。可将用户程序的任一页放在内存的任一块中,实现了离散分配。

    分段: 将用户程序地址空间分成若干个大小不等的段,每段可以定义一组相对完整的逻辑信息。存储分配时,以段为单位,段与段在内存中可以不相邻接,也实现了离散分配。

    分页与分段的主要区别

    1. 页是信息的物理单位,分页是为了实现非连续分配,以便解决内存碎片问题,或者说分页是由于系统管理的需要.段是信息的逻辑单位,它含有一组意义相对完整的信息,分段的目的是为了更好地实现共享,满足用户的需要.
    2. 页的大小固定,由系统确定,将逻辑地址划分为页号和页内地址是由机器硬件实现的.而段的长度却不固定,决定于用户所编写的程序,通常由编译程序在对源程序进行编译时根据信息的性质来划分.
    3. 分页的作业地址空间是一维的.分段的地址空间是二维的.

    8 页面置换算法

    1. 最佳置换算法OPT:不可能实现
    2. 先进先出FIFO
    3. 最近最久未使用算法LRU:最近一段时间里最久没有使用过的页面予以置换.
    4. clock算法

    9 边沿触发和水平触发

    边缘触发是指每当状态变化时发生一个 io 事件,条件触发是只要满足条件就发生一个 io 事件

    数据库

    1 事务

    数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。
    原子性、一致性、隔离性、持久性

    2 数据库索引

    聚集索引,非聚集索引,B-Tree,B+Tree,最左前缀原理

    非聚集索引:索引的数据区存储的是真实数据的地址,索引与数据分开

    3 Redis原理

    Redis是什么?

    1. 是一个完全开源免费的key-value内存数据库
    2. 通常被认为是一个数据结构服务器,主要是因为其有着丰富的数据结构 strings、map、 list、sets、 sorted sets

    Redis数据库

    ​ 通常局限点来说,Redis也以消息队列的形式存在,作为内嵌的List存在,满足实时的高并发需求。在使用缓存的时候,redis比memcached具有更多的优势,并且支持更多的数据类型,把redis当作一个中间存储系统,用来处理高并发的数据库操作

    • 速度快:使用标准C写,所有数据都在内存中完成,读写速度分别达到10万/20万
    • 持久化:对数据的更新采用Copy-on-write技术,可以异步地保存到磁盘上,主要有两种策略,一是根据时间,更新次数的快照(save 300 10 )二是基于语句追加方式(Append-only file,aof)
    • 自动操作:对不同数据类型的操作都是自动的,很安全
    • 快速的主–从复制,官方提供了一个数据,Slave在21秒即完成了对Amazon网站10G key set的复制。
    • Sharding技术: 很容易将数据分布到多个Redis实例中,数据库的扩展是个永恒的话题,在关系型数据库中,主要是以添加硬件、以分区为主要技术形式的纵向扩展解决了很多的应用场景,但随着web2.0、移动互联网、云计算等应用的兴起,这种扩展模式已经不太适合了,所以近年来,像采用主从配置、数据库复制形式的,Sharding这种技术把负载分布到多个特理节点上去的横向扩展方式用处越来越多。

    Redis缺点

    • 是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
    • Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。因此开发者必须合理利用有限内存,仅缓存热数据。
    • Redis的持久化并不是很靠谱
    • Redis 3.0推出了官方集群方案,但是也有很多优秀的开源解决方案,比如Codis

    Redis应用

    数据缓存

    • 以string类型存储序列化后的对象

    • 序列化方式要效率高、内存占用小

    • key的命名要唯一、有语义且尽可能的短

    • 数据一致性问题

      设置缓存过期时间、发现失效后清除缓存。

      一般在查询、新增对象后,将对象放入缓存;修改对象后更新缓存;删除对象后清除对应的缓存;

    消息队列

    Redis 中list的数据结构实现是双向链表,所以可以非常便捷的应用于消息队列(生产者 / 消费者模型)。消息的生产者只需要通过lpush将消息放入 list,消费者便可以通过rpop取出该消息,并且可以保证消息的有序性。如果需要实现带有优先级的消息队列也可以选择sorted set。而pub/sub功能也可以用作发布者 / 订阅者模型的消息。无论使用何种方式,由于 Redis 拥有持久化功能,也不需要担心由于服务器故障导致消息丢失的情况。

    时间轴

    list作为双向链表,不光可以作为队列使用。如果将它用作栈便可以成为一个公用的时间轴。当用户发完微博后,都通过lpush将它存放在一个 key 为LATEST_WEIBOlist中,之后便可以通过lrange取出当前最新的微博。

    排行榜

    使用sorted set和一个计算热度的算法便可以轻松打造一个热度排行榜,zrevrangebyscore可以得到以分数倒序排列的序列,zrank可以得到一个成员在该排行榜的位置(是分数正序排列时的位置,如果要获取倒序排列时的位置需要用zcard-zrank)。

    计数器

    计数功能应该是最适合 Redis 的使用场景之一了,因为它高频率读写的特征可以完全发挥 Redis 作为内存数据库的高效。在 Redis 的数据结构中,stringhashsorted set都提供了incr方法用于原子性的自增操作,下面举例说明一下它们各自的使用场景:

    • 如果应用需要显示每天的注册用户数,便可以使用string作为计数器,设定一个名为REGISTERED_COUNT_TODAY的 key,并在初始化时给它设置一个到凌晨 0 点的过期时间,每当用户注册成功后便使用incr命令使该 key 增长 1,同时当每天凌晨 0 点后,这个计数器都会因为 key 过期使值清零。
    • 每条微博都有点赞数、评论数、转发数和浏览数四条属性,这时用hash进行计数会更好,将该计数器的 key 设为weibo:weibo_idhash的 field 为like_numbercomment_numberforward_numberview_number,在对应操作后通过hincrby使hash 中的 field 自增。
    • 如果应用有一个发帖排行榜的功能,便选择sorted set吧,将集合的 key 设为POST_RANK。当用户发帖后,使用zincrby将该用户 id 的 score 增长 1。sorted set会重新进行排序,用户所在排行榜的位置也就会得到实时的更新。

    好友关系

    这个场景最开始是是一篇介绍微博 Redis 应用的 PPT 中看到的,其中提到微博的 Redis 主要是用在在计数和好友关系两方面上,当时对好友关系方面的用法不太了解,后来看到《Redis 设计与实现》中介绍到作者最开始去使用 Redis 便是希望能通过set解决传统数据库无法快速计算集合中交集这个功能。后来联想到微博当前的业务场景,确实能够以这种方式实现,所以姑且猜测一下:

    对于一个用户 A,将它的关注和粉丝的用户 id 都存放在两个 set 中:

    • A:follow:存放 A 所有关注的用户 id

    • A:follower:存放 A 所有粉丝的用户 id

      那么通过sinter命令便可以根据A:followA:follower的交集得到与 A 互相关注的用户。当 A 进入另一个用户 B 的主页后,A:followB:follow的交集便是 A 和 B 的共同专注,A:followB:follower的交集便是 A 关注的人也关注了 B。

    分布式锁

    在 Redis 2.6.12 版本开始,stringset命令增加了三个参数:

    • EX:设置键的过期时间(单位为秒)

    • PX:设置键的过期时间(单位为毫秒)

    • NX | XX:当设置为NX时,仅当 key 存在时才进行操作,设置为XX时,仅当 key 不存在才会进行操作

      由于这个操作是原子性的,可以简单地以此实现一个分布式的锁,例如:

    set key "lock" EX 1 XX

    如果这个操作返回false,说明 key 的添加不成功,也就是当前有人在占用这把锁。而如果返回true,则说明得了锁,便可以继续进行操作,并且在操作后通过del命令释放掉锁。并且即使程序因为某些原因并没有释放锁,由于设置了过期时间,该锁也会在 1 秒后自动释放,不会影响到其他程序的运行。

    倒排索引

    倒排索引是构造搜索功能的最常见方式,在 Redis 中也可以通过set进行建立倒排索引,这里以简单的拼音 + 前缀搜索城市功能举例:

    假设一个城市北京,通过拼音词库将北京转为beijing,再通过前缀分词将这两个词分为若干个前缀索引,有:北京bbebeijinbeijing。将这些索引分别作为set的 key(例如:index:北)并存储北京的 id,倒排索引便建立好了。接下来只需要在搜索时通过关键词取出对应的set并得到其中的 id 即可。

    4 乐观锁和悲观锁

    悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作

    乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。

    5 MVCC

    ​ 全称是Multi-Version Concurrent Control,即多版本并发控制,在MVCC协议下,每个读操作会看到一个一致性的snapshot,并且可以实现非阻塞的读。MVCC允许数据具有多个版本,这个版本可以是时间戳或者是全局递增的事务ID,在同一个时间点,不同的事务看到的数据是不同的。

    MySQL的innodb引擎是如何实现MVCC的

    innodb会为每一行添加两个字段,分别表示该行创建的版本删除的版本,填入的是事务的版本号,这个版本号随着事务的创建不断递增。在repeated read的隔离级别下,具体各种数据库操作的实现:

    • select:满足以下两个条件innodb会返回该行数据:
      • 该行的创建版本号小于等于当前版本号,用于保证在select操作之前所有的操作已经执行落地。
      • 该行的删除版本号大于当前版本或者为空。删除版本号大于当前版本意味着有一个并发事务将该行删除了。
    • insert:将新插入的行的创建版本号设置为当前系统的版本号。
    • delete:将要删除的行的删除版本号设置为当前系统的版本号。
    • update:不执行原地update,而是转换成insert + delete。将旧行的删除版本号设置为当前版本号,并将新行insert同时设置创建版本号为当前版本号。

    其中,写操作(insert、delete和update)执行时,需要将系统版本号递增。

    ​ 由于旧数据并不真正的删除,所以必须对这些数据进行清理,innodb会开启一个后台线程执行清理工作,具体的规则是将删除版本号小于当前系统版本的行删除,这个过程叫做purge。

    通过MVCC很好的实现了事务的隔离性,可以达到repeated read级别,要实现serializable还必须加锁。

    6 MyISAM和InnoDB

    MyISAM 适合于一些需要大量查询的应用,但其对于有大量写操作并不是很好。甚至你只是需要update一个字段,整个表都会被锁起来,而别的进程,就算是读进程都无法操作直到写操作完成。另外,MyISAM 对于 SELECT COUNT(*) 这类的计算是超快无比的。

    InnoDB 的趋势会是一个非常复杂的存储引擎,对于一些小的应用,它会比 MyISAM 还慢。他是它支持“行锁” ,于是在写操作比较多的时候,会更优秀。并且,他还支持更多的高级应用,比如:事务。

    网络

    1 三次握手

    1. 客户端通过向服务器端发送一个SYN来创建一个主动打开,作为三次握手的一部分。客户端把这段连接的序号设定为随机数 A。
    2. 服务器端应当为一个合法的SYN回送一个SYN/ACK。ACK 的确认码应为 A+1,SYN/ACK 包本身又有一个随机序号 B。
    3. 最后,客户端再发送一个ACK。当服务端受到这个ACK的时候,就完成了三路握手,并进入了连接创建状态。此时包序号被设定为收到的确认号 A+1,而响应则为 B+1。

    2 四次挥手

    注意: 中断连接端可以是客户端,也可以是服务器端. 下面仅以客户端断开连接举例, 反之亦然.

    1. 客户端发送一个数据分段, 其中的 FIN 标记设置为1. 客户端进入 FIN-WAIT 状态. 该状态下客户端只接收数据, 不再发送数据.
    2. 服务器接收到带有 FIN = 1 的数据分段, 发送带有 ACK = 1 的剩余数据分段, 确认收到客户端发来的 FIN 信息.
    3. 服务器等到所有数据传输结束, 向客户端发送一个带有 FIN = 1 的数据分段, 并进入 CLOSE-WAIT 状态, 等待客户端发来带有 ACK = 1 的确认报文.
    4. 客户端收到服务器发来带有 FIN = 1 的报文, 返回 ACK = 1 的报文确认, 为了防止服务器端未收到需要重发, 进入 TIME-WAIT 状态. 服务器接收到报文后关闭连接. 客户端等待 2MSL 后未收到回复, 则认为服务器成功关闭, 客户端关闭连接.

    3 ARP协议

    地址解析协议(Address Resolution Protocol),其基本功能为透过目标设备的IP地址,查询目标的MAC地址,以保证通信的顺利进行。它是IPv4网络层必不可少的协议,不过在IPv6中已不再适用,并被邻居发现协议(NDP)所替代。

    4 urllib和urllib2的区别

    这个面试官确实问过,当时答的urllib2可以Post而urllib不可以.

    1. urllib提供urlencode方法用来GET查询字符串的产生,而urllib2没有。这是为何urllib常和urllib2一起使用的原因。
    2. urllib2可以接受一个Request类的实例来设置URL请求的headers,urllib仅可以接受URL。这意味着,你不可以伪装你的User Agent字符串等。

    5 Post和Get

    6 Cookie和Session

    Cookie Session
    储存位置 客户端 服务器端
    目的 跟踪会话,也可以保存用户偏好设置或者保存用户名密码等 跟踪会话
    安全性 不安全 安全

    session技术是要使用到cookie的,之所以出现session技术,主要是为了安全。

    7 apache和nginx的区别

    nginx 相对 apache 的优点:

    • 轻量级,同样起web 服务,比apache 占用更少的内存及资源
    • 抗并发,nginx 处理请求是异步非阻塞的,支持更多的并发连接,而apache 则是阻塞型的,在高并发下nginx 能保持低资源低消耗高性能
    • 配置简洁
    • 高度模块化的设计,编写模块相对简单
    • 社区活跃

    apache 相对nginx 的优点:

    • rewrite ,比nginx 的rewrite 强大
    • 模块超多,基本想到的都可以找到
    • 少bug ,nginx 的bug 相对较多
    • 超稳定

    8 网站用户密码保存

    1. 明文保存
    2. 明文hash后保存,如md5
    3. MD5+Salt方式,这个salt可以随机
    4. 知乎使用了Bcrypy(好像)加密

    9 HTTP和HTTPS

    状态码 定义
    1xx 报告 接收到请求,继续进程
    2xx 成功 步骤成功接收,被理解,并被接受
    3xx 重定向 为了完成请求,必须采取进一步措施
    4xx 客户端出错 请求包括错的顺序或不能完成
    5xx 服务器出错 服务器无法完成显然有效的请求

    403: Forbidden
    404: Not Found

    HTTPS握手,对称加密,非对称加密,TLS/SSL,RSA

    10 XSRF和XSS

    • CSRF(Cross-site request forgery)跨站请求伪造
    • XSS(Cross Site Scripting)跨站脚本攻击

    CSRF重点在请求,XSS重点在脚本

    11 幂等 Idempotence

    HTTP方法的幂等性是指一次和多次请求某一个资源应该具有同样的副作用。(注意是副作用)

    GET http://www.bank.com/account/123456,不会改变资源的状态,不论调用一次还是N次都没有副作用。请注意,这里强调的是一次和N次具有相同的副作用,而不是每次GET的结果相同。GET http://www.news.com/latest-news这个HTTP请求可能会每次得到不同的结果,但它本身并没有产生任何副作用,因而是满足幂等性的。

    DELETE方法用于删除资源,有副作用,但它应该满足幂等性。比如:DELETE http://www.forum.com/article/4231,调用一次和N次对系统产生的副作用是相同的,即删掉id为4231的帖子;因此,调用者可以多次调用或刷新页面而不必担心引起错误。

    POST所对应的URI并非创建的资源本身,而是资源的接收者。比如:POST http://www.forum.com/articles的语义是在http://www.forum.com/articles下创建一篇帖子,HTTP响应中应包含帖子的创建状态以及帖子的URI。两次相同的POST请求会在服务器端创建两份资源,它们具有不同的URI;所以,POST方法不具备幂等性。

    PUT所对应的URI是要创建或更新的资源本身。比如:PUT http://www.forum/articles/4231的语义是创建或更新ID为4231的帖子。对同一URI进行多次PUT的副作用和一次PUT是相同的;因此,PUT方法具有幂等性。

    12 RESTful架构(SOAP,RPC)

    REST (Representational State Transfer) 是一种软件架构风格。它将服务端的信息和功能等所有事物统称为资源,客户端的请求实际就是对资源进行操作,它的主要特点有: 每一个资源都会对应一个独一无二的 url , 客户端通过 HTTP 的 GET、POST、PUT、DELETE 请求方法对资源进行查询、创建、修改、删除操作 - 客户端与服务端的交互必须是无状态的

    URI表示资源,HTTP的方法表示对资源的操作。

    13 SOAP

    SOAP(原为Simple Object Access Protocol的首字母缩写,即简单对象访问协议)是交换数据的一种协议规范,使用在计算机网络Web服务(web service)中,交换带结构信息。SOAP为了简化网页服务器(Web Server)从XML数据库中提取数据时,节省去格式化页面时间,以及不同应用程序之间按照HTTP通信协议,遵从XML格式执行资料互换,使其抽象于语言实现、平台和硬件。

    14 RPC

    RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。

    总结:服务提供的两大流派.传统意义以方法调用为导向通称RPC。为了企业SOA,若干厂商联合推出webservice,制定了wsdl接口定义,传输soap.当互联网时代,臃肿SOA被简化为http+xml/json.但是简化出现各种混乱。以资源为导向,任何操作无非是对资源的增删改查,于是统一的REST出现了.

    进化的顺序: RPC -> SOAP -> RESTful

    15 CGI和WSGI

    CGI是通用网关接口,是连接web服务器和应用程序的接口,用户通过CGI来获取动态数据或文件等。
    CGI程序是一个独立的程序,它可以用几乎所有语言来写,包括perl,c,lua,python等等。

    WSGI, Web Server Gateway Interface,是Python应用程序或框架和Web服务器之间的一种接口,WSGI的其中一个目的就是让用户可以用统一的语言(Python)编写前后端。

    16 中间人攻击

    在GFW里屡见不鲜的,呵呵.

    中间人攻击(Man-in-the-middle attack,通常缩写为MITM)是指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者完全控制。

    17 c10k问题

    所谓c10k问题,指的是服务器同时支持成千上万个客户端的问题,也就是concurrent 10 000 connection(这也是c10k这个名字的由来)。

    • 异步非阻塞,如基于epoll原理的编程
    • 分布式系统

    18 socket

    Socket=Ip address+ TCP/UDP + port

    19 浏览器缓存

    304 Not Modified

    20 HTTP1.0和HTTP1.1

    新的特点:请求头有Host字段,因此一个服务器可部署多个网站;长链接;文件断点续传;身份认证,状态管理,Cache缓存

    HTTP/1.1协议中共定义了8种HTTP请求方法,不同的方法规定了不同的操作指定的资源方式。服务端也会根据不同的请求方法做不同的响应。

    • GET :一般只用于数据的读取,而不应当用于会产生副作用的非幂等的操作中。
    • HEAD: 只请求服务器的响应头信息。
    • POST: 向指定资源提交数据如:表单数据提交、文件上传等,请求数据会被包含在请求体中。POST方法是非幂等的方法,因为这个请求可能会创建新的资源或/和修改现有资源。
    • PUT: 向指定资源位置上传其最新内容,PUT方法是幂等的方法。通过该方法客户端可以将指定资源的最新数据传送给服务器取代指定的资源的内容。
    • DELETE: 用于请求服务器删除所请求URI所标识的资源。DELETE方法也是幂等的。
    • CONNECT: HTTP/1.1协议预留的,能够将连接改为管道方式的代理服务器。通常用于SSL加密服务器的链接与非加密的HTTP代理服务器的通信。
    • OPTIONS: 请求服务器返回该资源所支持的所有HTTP请求方法,该方法会用’*’来代替资源名称,向服务器发送OPTIONS请求,可以测试服务器功能是否正常。JavaScript的XMLHttpRequest对象进行CORS跨域资源共享时,就是使用OPTIONS方法发送嗅探请求,以判断是否有对指定资源的访问权限。
    • TRACE: 请求服务器回显其收到的请求信息,该方法主要用于HTTP请求的测试或诊断
    • PATCH:1.1新增的方法,同PUT一样,用于资源的更新,PATCH一般用于资源的部分更新,而PUT一般用于资源的整体更新。 当资源不存在时,PATCH会创建一个新的资源,而PUT只会对已在资源进行更新。

    21 Ajax

    AJAX,Asynchronous JavaScript and XML(异步的 JavaScript 和 XML), 是与在不重新加载整个页面的情况下,与服务器交换数据并更新部分网页的技术。

    UNIX

    unix进程间通信方式(IPC)

    1. 管道(Pipe):管道可用于具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信。
    2. 命名管道(named pipe):命名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。
    3. 信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。
    4. 消息(Message)队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺
    5. 共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
    6. 内存映射(mapped memory):内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它。
    7. 信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
    8. 套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。

    数据结构

    1 红黑树

    红黑树与AVL的比较:

    AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多;

    红黑是用非严格的平衡来换取增删节点时候旋转次数的降低;

    所以简单说,如果你的应用中,搜索的次数远远大于插入和删除,那么选择AVL,如果搜索,插入删除次数几乎差不多,应该选择RB。

    编程题

    1 台阶问题/斐波那契

    一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

    1
    fib = lambda n: n if n <= 2 else fib(n - 1) + fib(n - 2)

    第二种记忆方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    def memo(func):
    cache = {}
    def wrap(*args):
    if args not in cache:
    cache[args] = func(*args)
    return cache[args]
    return wrap
    @memo
    def fib(i):
    if i < 2:
    return 1
    return fib(i-1) + fib(i-2)

    第三种方法

    1
    2
    3
    4
    5
    def fib(n):
    a, b = 0, 1
    for _ in xrange(n):
    a, b = b, a + b
    return b

    2 变态台阶问题

    一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

    1
    fib = lambda n: n if n < 2 else 2 * fib(n - 1)

    3 矩形覆盖

    我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?

    2*n个矩形的覆盖方法等于第2*(n-1)加上第2*(n-2)的方法。

    1
    f = lambda n: 1 if n < 2 else f(n - 1) + f(n - 2)

    4 杨氏矩阵查找

    在一个m行n列二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

    使用Step-wise线性搜索。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    def get_value(l, r, c):
    return l[r][c]
    def find(l, x):
    m = len(l) - 1
    n = len(l[0]) - 1
    r = 0
    c = n
    while c >= 0 and r <= m:
    value = get_value(l, r, c)
    if value == x:
    return True
    elif value > x:
    c = c - 1
    elif value < x:
    r = r + 1
    return False

    5 去除列表中的重复元素

    用集合

    1
    list(set(l))

    用字典

    1
    2
    3
    l1 = ['b','c','d','b','c','a','a']
    l2 = {}.fromkeys(l1).keys()
    print l2

    用字典并保持顺序

    1
    2
    3
    4
    l1 = ['b','c','d','b','c','a','a']
    l2 = list(set(l1))
    l2.sort(key=l1.index)
    print l2

    列表推导式

    1
    2
    3
    l1 = ['b','c','d','b','c','a','a']
    l2 = []
    [l2.append(i) for i in l1 if not i in l2]

    sorted排序并且用列表推导式.

    l = [‘b’,’c’,’d’,’b’,’c’,’a’,’a’]
    [single.append(i) for i in sorted(l) if i not in single]
    print single

    6 链表成对调换

    1->2->3->4转换成2->1->4->3.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class ListNode:
    def __init__(self, x):
    self.val = x
    self.next = None
    class Solution:
    # @param a ListNode
    # @return a ListNode
    def swapPairs(self, head):
    if head != None and head.next != None:
    next = head.next
    head.next = self.swapPairs(next.next)
    next.next = head
    return next
    return head

    7 创建字典的方法

    1 直接创建

    1
    dict = {'name':'earth', 'port':'80'}

    2 工厂方法

    1
    2
    3
    items=[('name','earth'),('port','80')]
    dict2=dict(items)
    dict1=dict((['name','earth'],['port','80']))

    3 fromkeys()方法

    1
    2
    3
    4
    dict1={}.fromkeys(('x','y'),-1)
    dict={'x':-1,'y':-1}
    dict2={}.fromkeys(('x','y'))
    dict2={'x':None, 'y':None}

    8 合并两个有序列表

    尾递归

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    def _recursion_merge_sort2(l1, l2, tmp):
    if len(l1) == 0 or len(l2) == 0:
    tmp.extend(l1)
    tmp.extend(l2)
    return tmp
    else:
    if l1[0] < l2[0]:
    tmp.append(l1[0])
    del l1[0]
    else:
    tmp.append(l2[0])
    del l2[0]
    return _recursion_merge_sort2(l1, l2, tmp)
    def recursion_merge_sort2(l1, l2):
    return _recursion_merge_sort2(l1, l2, [])

    循环算法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def loop_merge_sort(l1, l2):
    tmp = []
    while len(l1) > 0 and len(l2) > 0:
    if l1[0] < l2[0]:
    tmp.append(l1[0])
    del l1[0]
    else:
    tmp.append(l2[0])
    del l2[0]
    tmp.extend(l1)
    tmp.extend(l2)
    return tmp

    pop弹出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    a = [1,2,3,7]
    b = [3,4,5]
    def merge_sortedlist(a,b):
    c = []
    while a and b:
    if a[0] >= b[0]:
    c.append(b.pop(0))
    else:
    c.append(a.pop(0))
    while a:
    c.append(a.pop(0))
    while b:
    c.append(b.pop(0))
    return c
    print merge_sortedlist(a,b)

    9 交叉链表求交点

    其实思想可以按照从尾开始比较两个链表,如果相交,则从尾开始必然一致,只要从尾开始比较,直至不一致的地方即为交叉点,如图所示

    我觉得用栈更简单。

    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
    #coding:utf-8
    class ListNode:
    def __init__(self, x):
    self.val = x
    self.next = None
    def node(l1, l2):
    length1, length2 = 0, 0
    # 求两个链表长度
    while l1.next:
    l1 = l1.next#尾节点
    length1 += 1
    while l2.next:
    l2 = l2.next#尾节点
    length2 += 1
    #如果相交
    if l1.next == l2.next:
    # 长的链表先走
    if length1 > length2:
    for _ in range(length1 - length2):
    l1 = l1.next
    return l1#返回交点
    else:
    for _ in range(length2 - length1):
    l2 = l2.next
    return l2#返回交点
    # 如果不相交
    else:
    return

    10 二分查找

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #coding:utf-8
    def binary_search(list,item):
    low = 0
    high = len(list)-1
    while low<=high:
    mid = (low+high)/2
    guess = list[mid]
    if guess>item:
    high = mid-1
    elif guess<item:
    low = mid+1
    else:
    return mid
    return None
    mylist = [1,3,5,7,9]
    print binary_search(mylist,3)

    11 快排

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #coding:utf-8
    def quicksort(list):
    if len(list)<2:
    return list
    else:
    midpivot = list[0]
    lessbeforemidpivot = [i for i in list[1:] if i<=midpivot]
    biggerafterpivot = [i for i in list[1:] if i > midpivot]
    finallylist = quicksort(lessbeforemidpivot)+[midpivot]+quicksort(biggerafterpivot)
    return finallylist
    print quicksort([2,4,6,7,1,2,5])

    12 找零问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #coding:utf-8
    #values是硬币的面值values = [ 25, 21, 10, 5, 1]
    #valuesCounts 钱币对应的种类数
    #money 找出来的总钱数
    #coinsUsed 对应于目前钱币总数i所使用的硬币数目
    def coinChange(values,valuesCounts,money,coinsUsed):
    #遍历出从1到money所有的钱数可能
    for cents in range(1,money+1):
    minCoins = cents
    #把所有的硬币面值遍历出来和钱数做对比
    for kind in range(0,valuesCounts):
    if (values[kind] <= cents):
    temp = coinsUsed[cents - values[kind]] +1
    if (temp < minCoins):
    minCoins = temp
    coinsUsed[cents] = minCoins
    print ('面值:{0}的最少硬币使用数为:{1}'.format(cents, coinsUsed[cents]))

    13 广度遍历和深度遍历二叉树

    给定一个数组,构建二叉树,并且按层次打印这个二叉树

    14 二叉树节点

    1
    2
    3
    4
    5
    6
    7
    8
    class Node(object):
    def __init__(self, data, left=None, right=None):
    self.data = data
    self.left = left
    self.right = right
    tree = Node(1, Node(3, Node(7, Node(0)), Node(6)), Node(2, Node(5), Node(4)))

    15 层次遍历

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def lookup(root):
    stack = [root]
    while stack:
    current = stack.pop(0)
    print current.data
    if current.left:
    stack.append(current.left)
    if current.right:
    stack.append(current.right)

    16 深度遍历

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    def deep(root):
    if not root:
    return
    print root.data
    deep(root.left)
    deep(root.right)
    if __name__ == '__main__':
    lookup(tree)
    deep(tree)

    17 前中后序遍历

    深度遍历改变顺序就OK了

    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
    #coding:utf-8
    #二叉树的遍历
    #简单的二叉树节点类
    class Node(object):
    def __init__(self,value,left,right):
    self.value = value
    self.left = left
    self.right = right
    #中序遍历:遍历左子树,访问当前节点,遍历右子树
    def mid_travelsal(root):
    if root.left is None:
    mid_travelsal(root.left)
    #访问当前节点
    print(root.value)
    if root.right is not None:
    mid_travelsal(root.right)
    #前序遍历:访问当前节点,遍历左子树,遍历右子树
    def pre_travelsal(root):
    print (root.value)
    if root.left is not None:
    pre_travelsal(root.left)
    if root.right is not None:
    pre_travelsal(root.right)
    #后续遍历:遍历左子树,遍历右子树,访问当前节点
    def post_trvelsal(root):
    if root.left is not None:
    post_trvelsal(root.left)
    if root.right is not None:
    post_trvelsal(root.right)
    print (root.value)

    18 求最大树深

    1
    2
    3
    4
    def maxDepth(root):
    if not root:
    return 0
    return max(maxDepth(root.left), maxDepth(root.right)) + 1

    19 求两棵树是否相同

    1
    2
    3
    4
    5
    6
    7
    def isSameTree(p, q):
    if p == None and q == None:
    return True
    elif p and q :
    return p.val == q.val and isSameTree(p.left,q.left) and isSameTree(p.right,q.right)
    else :
    return False

    20 前序中序求后序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    def rebuild(pre, center):
    if not pre:
    return
    cur = Node(pre[0])
    index = center.index(pre[0])
    cur.left = rebuild(pre[1:index + 1], center[:index])
    cur.right = rebuild(pre[index + 1:], center[index + 1:])
    return cur
    def deep(root):
    if not root:
    return
    deep(root.left)
    deep(root.right)
    print root.data

    21 单链表逆置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Node(object):
    def __init__(self, data=None, next=None):
    self.data = data
    self.next = next
    link = Node(1, Node(2, Node(3, Node(4, Node(5, Node(6, Node(7, Node(8, Node(9)))))))))
    def rev(link):
    pre = link
    cur = link.next
    pre.next = None
    while cur:
    tmp = cur.next
    cur.next = pre
    pre = cur
    cur = tmp
    return pre
    root = rev(link)
    while root:
    print root.data
    root = root.next

    22 两个字符串是否是变位词

    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
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    class Anagram:
    """
    @:param s1: The first string
    @:param s2: The second string
    @:return true or false
    """
    def Solution1(s1,s2):
    alist = list(s2)
    pos1 = 0
    stillOK = True
    while pos1 < len(s1) and stillOK:
    pos2 = 0
    found = False
    while pos2 < len(alist) and not found:
    if s1[pos1] == alist[pos2]:
    found = True
    else:
    pos2 = pos2 + 1
    if found:
    alist[pos2] = None
    else:
    stillOK = False
    pos1 = pos1 + 1
    return stillOK
    print(Solution1('abcd','dcba'))
    def Solution2(s1,s2):
    alist1 = list(s1)
    alist2 = list(s2)
    alist1.sort()
    alist2.sort()
    pos = 0
    matches = True
    while pos < len(s1) and matches:
    if alist1[pos] == alist2[pos]:
    pos = pos + 1
    else:
    matches = False
    return matches
    print(Solution2('abcde','edcbg'))
    def Solution3(s1,s2):
    c1 = [0]*26
    c2 = [0]*26
    for i in range(len(s1)):
    pos = ord(s1[i])-ord('a')
    c1[pos] = c1[pos] + 1
    for i in range(len(s2)):
    pos = ord(s2[i])-ord('a')
    c2[pos] = c2[pos] + 1
    j = 0
    stillOK = True
    while j<26 and stillOK:
    if c1[j] == c2[j]:
    j = j + 1
    else:
    stillOK = False
    return stillOK
    print(Solution3('apple','pleap'))

    23 动态规划问题

    背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,如何选择才能使得物品的总价格最高。

    01背包问题

    问题描述:有编号分别为a,b,c,d,e的五件物品,它们的重量分别是2,2,6,5,4,它们的价值分别是6,3,5,4,6,每件物品数量只有一个,现在给你个承重为10的背包,如何让背包里装入的物品具有最大的价值总和?

    基本思路:将该问题转换成子问题,考虑五件物品在给定承重 C 的背包下最大价值为原问题,如下表所示,即为考虑abcde,C = 10时的最大价值,假设为f[5][10],原问题的解可以分解为两种情况,第一种情况是不考虑放入a只考虑放入bcde承重为C时的最大价值f[4][C],第二种情况是考虑放入a时的最大价值,即value[a]+f[4][10-weight[a]]。 原问题的解f[5][10]取上述两种情况中的最大值,即f[5][10] = max{f[4][10], (f[4][10-weight[a]+value[a]))}。 由此可以看出里面涉及到需要计算f[4][10]f[4][10-weight[a]]f[4][4]等子问题。 以此类推,自顶向下的分析可以看出原问题需要子问题的解,我们需要先计算出子问题的解,自底向上求解。求解方式如下表所示,顺序是自底向上、从左往右,或者从左往右、自底向上都可以。注意此问题中的abcde可以包含相同的物件,它们之间的顺序也可以是任意的,不影响最终的结果。

    name weight value 0 1 2 3 4 5 6 7 8 9 10
    a 2 6 0 0 6 6 9 9 12 12 15 15 15
    b 2 3 0 0 3 3 6 6 9 9 9 10 11
    c 6 5 0 0 0 0 6 6 6 6 6 10 11
    d 5 4 0 0 0 0 6 6 6 6 6 10 10
    e 4 6 0 0 0 0 6 6 6 6 6 6 6

    按上述描述的递推公式计算时,我们需要初始值,f[i][0]=0(1<=i<=5), f[1][j]=(j < weight[e])?0:value[e],(1<=j<=10)。

    完全背包问题

    问题描述:有编号分别为a,b,c,d的四件物品,它们的重量分别是2,3,4,7,它们的价值分别是1,3,5,9,每件物品数量无限个,现在给你个承重为10的背包,如何让背包里装入的物品具有最大的价值总和?

    思路: 问题解法其实和01背包问题一样,只是初始化的值和递推公式需要稍微变化一下。初始化时,当只考虑一件物品a时,f[1][j] = j/weight[a]。 递推公式计算时,f[i][y] = max{f[i-1][y], (f[i][y-weight[i]]+value[i])},注意这里当考虑放入一个物品 i 时应当考虑还可能继续放入 i,因此这里是f[i][y-weight[i]]+value[i], 而不是f[i-1][y-weight[i]]+value[i]

    name weight value 0 1 2 3 4 5 6 7 8 9 10
    d 7 9 0 0 1 3 5 5 6 9 10 10 12
    c 4 5 0 0 1 3 5 5 6 8 10 10 11
    b 3 3 0 0 1 3 3 4 6 6 7 9 9
    a 2 1 0 0 1 1 2 2 3 3 4 4 5

    多重背包问题

    问题描述:有编号分别为a,b,c的三件物品,它们的重量分别是1,2,2,它们的价值分别是6,10,20,他们的数目分别是10,5,2,现在给你个承重为 8 的背包,如何让背包里装入的物品具有最大的价值总和?

    解题思路

    1. 作为一个新问题考虑,由于每个物品多了数目限制,因此初始化和递推公式都需要更改一下。初始化时,只考虑一件物品a时,f[1][j] = min{num[1], j/weight[1]}。 计算考虑i件物品承重限制为y时最大价值f[i][y]时,递推公式考虑两种情况,要么第 i 件物品一件也不放,就是f[i-1][y], 要么第 i 件物品放 k 件,其中 1 <= k <= (y/weight[i]),考虑这一共 k+1 种情况取其中的最大价值即为f[i][y]的值,即f[i][y] = max{f[i-1][y], (f[i-1][y-k*weight[i]]+k*value[i])}。 这里为什么不能像完全背包一样直接考虑f[i][y-weight[i]]+value[i]呢?因为这样不容易判断第 i 件物品的个数是否超过限制数量 num[i]

      | name | weight | value | num | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
      | —- | —— | —– | —- | —- | —- | —- | —- | —- | —- | —- | —- | —- |
      | c | 2 | 20 | 2 | 0 | 6 | 20 | 26 | 40 | 46 | 52 | 58 | 64 |
      | b | 2 | 10 | 5 | 0 | 6 | 12 | 18 | 24 | 30 | 36 | 42 | 48 |
      | a | 1 | 6 | 10 | 0 | 6 | 12 | 18 | 24 | 30 | 36 | 42 | 48 |

    2. 由01背包的分析可知,01背包中允许放入的物品有重复,即01背包中如果考虑要放入的物品的重量和价格相同,不影响最终的结果,因为我们可以考虑把多重背包问题中限制数目的物品拆分成单独的一件件物品,作为01背包问题考虑。问题解法和01背包一致。

    Python Web后端面试

    后端面试主要是三个方面:python语言,web框架,数据库

    Git Flow工作流程

    主要分支

    mark

    • master : 主分支,用于对外发布产品
    • develop: 开发分支,从master克隆代码后,进行开发工作
    • release: 测试分支,在开发完成后,从develop分支克隆到本分支测试,小的bug直接修复后合并到develop。作为临时分支,测试完成后可以删除
    • bugfix: 基于master分支或发布的Tag克隆,用于修复收到的bug反馈。修复完成后分别合并到develop和master里。
    • feature:功能分支,基于develop克隆,用于探索性功能或多人协助开发,开发完成后合并到develop分支。

    开发流程

    1. 进入本地仓库工作区,从远程仓库克隆代码到本地

    2. 基于master分支,创建develop分支

      1
      2
      3
      4
      5
      6
      /* 切换到master分支 */
      $git checkout master
      /* 基于master分支克隆develop分支,并在克隆完毕后直接跳转到develop分支 */
      $git checkout -b develop
      /* 推送develop分支到远程仓库 */
      $git push origin develop
    3. 本地开发

      1
      2
      3
      4
      5
      6
      7
      8
      /* 提交修改到缓冲区 */
      $git add .
      /* 提交修改到本地仓库 */
      /* 如果是修复的BUG,应该在修改说明的最开始添加Bug#ID,多个Bug用逗号分隔,例如Bug#002,003 */
      /* 如果是完成了一个指派的任务,应该在修改说明的最开始添加Task#TaskID,例如Task#165 */
      $git commit -m "Bug#123 修改说明"
      /* 每完成一个功能点可以对代码进行打包 */
      $git tag -m "简要说明增加/修复/删除了什么功能" v0.0.0.170718

      实现一个功能或一天工作结束时,提交代码。不是每一个Tag都需要提交到远程仓库,在编译成功&测试通过后,再打一个新的Tag包(里程碑Tag包),仅将里程碑Tag包推送到远程仓库

    4. 推送代码到远程仓库

      当完成一个功能点或阶段工作时,将代码推送到远程仓库develop分支

      1
      2
      3
      4
      /* 执行代码拉取操作,防止代码冲突 */
      $git pull
      /* 解决代码冲突后,推送代码到远程仓库*/
      $git push origin develop1234

      注:禁止将未编译或编译不通过的代码提交到远程仓库,如果编码工作进行未完成可以提交到本地仓库中,等待该功能点全部实现后再将代码推送到远程仓库中。

    5. 将代码发布到测试分支

      阶段性的开发工作已完成,启动小批量测试工作,将代码发布到测试分支release

      1
      2
      3
      $git checkout develop
      $git checkout -b release
      $git push origin release
    6. 修复测试提交的bug

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      /* 克隆仓库 */
      $git clone https://admin@192.168.1.88:8443/r/admin/test.$git
      /* 查看远程仓库分支情况:克隆仓库时只能克隆master分支,因此需要拉取指定分支,我们使用$git branch -r查看远程分支情况 */
      $git branch -r
      origin/HEAD -> origin/master
      origin/dev
      origin/master
      origin/release
      /* 拉取测试分支 */
      $git checkout -b release origin/release

      在commit提交时,要在修复说明中添加Bug#bugID及相关说明。

    7. 测试完成后,代码合并到develop分支

      1
      2
      3
      4
      5
      /* 切换到develop分支 */
      $git checkout develop
      /* 执行合并操作,将release分支代码合并到develop分支 */
      $git merge release
      /* 如果合并报错,则解决冲突,冲突解决后继续再次执行合并 */

    8. 开发、测试完成后,将develop分支合并到master,进行版本发布,同时打一个里程碑Tag包

      1
      2
      3
      4
      5
      6
      7
      8
      # 合并
      $git checkout master
      $git merge develop
      # 打Tag
      /* 创建里程碑Tag */
      $git tag -m "Task#003 v1.0.0 首版发布" v1.0.0.170718
      /* 推送里程碑Tag到远程仓库 */
      $git push origin v1.0.0.170718

    发布后Bug修复

    1. 获取Bug产品的版本号

    2. 查看里程碑Tag,并基于出bug的里程碑创建分支

      1
      2
      3
      4
      /* 查询里程碑及其提交说明 */
      $git tag -n1 -l v*
      /* git checkout -b [创建的分支名称] [里程碑Tag名称] */
      $git checkout -b bugfix-v1.0.0.170718 v1.0.0.170718
    3. 修复代码后查看修改的地方,然后合并到develop和master

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      $git diff
      /* 合并到develop */
      $git checkout develop
      $git merge hotfix-v1.0.0.170718
      /* 提交到远程仓库develop分支 */
      $git push origin develop
      /* 合并到master:如果随下一个版本再发布,可不用合并至master分支 */
      $git checkout master
      $git merge develop
      /* 提交到远程仓库master分支 */
      $git push origin master
    4. 创建新的里程碑,删除bugfix分支

      1
      2
      3
      $git tag -m "Bug#002 修复某某Bug" v1.0.1.170719
      /* 推送到远程仓库 */
      $git push origin v1.0.1.170719

      / 删除本地分支-$git branch -d [本地分支名]/
      $git branch -d bugfix-v1.0.0.170718
      / 删除远程分支-$git push origin :[远程分支名]/
      $git push origin :bugfix-v1.0.0.170718

    撤销操作

    • commit以后发现有几个文件没提交,可以对上次提交修正,而不是新发起一个提交

      1
      2
      3
      4
      5
      6
      /* 正常提交 */
      $git commit -m "发布v1.0"
      /* 发现漏了文件,重新添加 */
      $git add CHANGELOG.md
      /* 重新提交,仍以"发布v1.0的名义提交",最终只有一个提交*/
      $git commit --amend
    • 撤销上次提交,但是保留暂存区和当前修改不变 $git reset --soft HEAD~

    • 撤销上次提交和暂存区修改,仅保留当前修改不变 $git reset --mixed HEAD~

    • 撤销上次提交,丢弃所有修改,并回档到上上次的提交 $git reset --hard HEAD~

    • 整体回档到指定的版本,谨慎使用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      /* 正常提交 */
      $git commit -m "发布v1.0"
      /* 修改多个文件 */
      /* 添加到暂存区 */
      git add .
      /* 撤销暂存区和本地目录下所有文件的修改,并整体回档到上一次提交的状态 */
      $git reset --hard HEAD
      /* 可以修改HEAD为SHA-1值回档到任意版本 */
      /* 使用git log查看每次提交的SHA-1值,可以仅指定前7位 */
      $git reset --hard 745d8cd
    • git add后撤回

      1
      2
      3
      4
      /* 添加文件到暂存区 */
      $git add README
      /* 将文件从暂存区撤回 */
      $git reset HEAD README
    • 撤销对文件的修改,恢复成上次提交的样子

      1
      2
      3
      /* 撤销对CHANGELOG.md文件的修改,请注意这是一个危险的命令,
      * 对指定文件的修改都会被取消,会还原成上次提交的样子 */
      $git checkout -- CHANGELOG.md

    Job1

    职位描述

    1. 开发和维护平台系统
    2. 负责web数据中心和运营中心功能的开发
    3. 配合团队其他成员进行模块开发及整合

    任职要求

    1. Python及其他开发语言
    2. 熟悉Tornado,Twisted,Libevent等异步socket通讯框架
    3. 熟练使用MySQL,熟悉MongoDB,Redis,具有面向对象设计思想,接口清晰,命名规范
    4. 熟悉Linux开发平台

    过程

    1. 自我介绍
    2. 谈一下学习后端的感想
    3. GIL是什么,怎么解决GIL带来的问题
    4. 装饰器是什么,应用场景是什么
    5. 闭包是什么
    6. 数据库原生语句会写吗,事务了解吗
    7. 编译原理了解吗

    Job2

    工作职责

    1. 参与网站的平台开发,架构设计和维护
    2. 参与需求分析和产品设计
    3. 撰写Python,Django进行后台及API的开发
    4. 撰写测试以及团队成员间进行Code Review
    5. 负责产品的开发,测试以及日后的维护升级

    任职要求

    1. 2年以上Python工作职责
    2. 熟悉互联网项目开发,熟悉Python以及Django
    3. 良好的技术相关英文能力
    4. 熟悉Git及Git Flow开发流程
    5. 逻辑抽象能力强,具有良好的沟通能力
    6. 熟悉Linux操作系统

    过程

    1. redis持久化,如果redis现需要重启,rdb模式下怎么在重启前保存数据
    2. mysql怎么处理高并发
    3. 说一下myisam和innodb的锁分别是什么
    4. get post区别
    5. git多人开发下的使用, 工作流
    6. nginx作用
    7. 三个web框架区别(flask,django,tornado)

    Job3

    职位描述

    1. 负责服务端软件的设计,开发,维护
    2. 负责后台数据库,程序模块的设计和开发;维护优化产品,进行性能优化和架构优化
    3. 负责基础架构组件开发和优化,负责开发和维护平台公用组件和模块。负责和前端/app集成的接口及联调

    任职要求

    1. 计算机基础知识扎实,包括不限定于操作系统,网络,数据结构和算法
    2. 熟练掌握至少一门语言:Ruby,Python
    3. 有些测试的习惯,熟悉单元测试和集成测试
    4. 熟悉MySql,Redis
    5. 了解RESTful API接口和常用的充值,支付,分享等第三方服务
    6. 了解html5,css3,bootstrap,angular js等前端框架
    7. 良好的编码习惯,良好的团队合作精神和学习能力,做事严谨踏实,责任心强

    过程

    1. 介绍一下项目,某个项目数据库怎么设计的
    2. mysql高可用
    3. redis,mysql,nginx
    4. tornado和django区别
    5. 符合restful的url是什么样子

    Job4

    职位描述

    1. 参与产品,架构设计和相关技术文档的完善
    2. 根据产品需求,完成基于Python的系统或模块的设计和开发
    3. 与各部门团队成员协同配合,完成迭代开发任务,确保产品的顺利上线

    岗位要求

    1. 熟悉掌握web后端开发,熟悉后端开发常用技术如:缓存,队列等
    2. 熟悉Linux基本操作
    3. 具备XML,RESTful或WebService接口开发经验

    过程

    装饰器
    谈一下tornado

    Job5

    职位描述

    1. 根据业务,梳理产品需求并合理设计后端服务开发流程
    2. 后端设计与开发以服务Web端,手机端,公众平台,小程序等
    3. 后端服务的自动化测试,运维及性能优化

    职位要求

    1. 本科,专业计算机
    2. 一年以上Python后端开发经验,软件知识结构全面,有成熟作品优先
    3. 熟悉Linux操作系统下开发,部署,维护
    4. 精通Python服务端编程,精通网络编程
    5. 精通Pythonn爬虫系统编程
    6. 熟练掌握一种以上Web开发框架
    7. 熟悉SQL语言
    8. 熟悉缓存机制以及NoSQL数据库,有redis开发经验优先
    9. 能对某个新领域进行搜索,总结并快速学习,有能力维护个人技术博客优先 加分项:
    10. 有良好的编程习惯与规范
    11. 有支付系统开发经验的优先
    12. 熟练使用Git
    13. 熟练运用阿里云或UCloud等云部署

    过程

    1. 介绍一下自己
    1. git熟悉吗
    1. http方法有哪些
    1. 谈谈restful
    1. 正则熟悉吗,匹配一个邮箱
    1. redis熟悉吗,怎么使用的
    2. python3要用python2的包怎么办
    3. 存储过程了解吗
    4. 1000万个数据里面删掉1000条,如何优化
    5. nginx是什么,负载均衡什么意思
    6. 多台设备登录,怎么实现告知先登录设备账号异地登录(后来谈到tornado里面的websocket,websocket是什么,websocket断线重连)
    7. 排序算法了解吗
    8. 前后端分离

    Spring boot 入门

    Spring Boot 入门

    spring boot是可以基于maven项目快速搭建SSM/H(或者其他Spring相关的项目)的开发框架。
    可以简化SSM框架的一系列配置,从而快速开发。