注意:对于单链表来说,添加一个无用的头节点,再对链表进行增删操作,可以忽略头尾的特殊情况,使代码统一
解决方案
type listNode struct{
val int
next *listNode
}
func newListNode(val int)*listNode{
return &listNode{
val:val,
next:nil,
}
}
type list struct{
head *listNode
length int
}
func newList()*list{
return &list{
head:newListNode(-1),
length:0,
}
}
/*--------------------------插入-----------------------------------*/
// 头插
func (l *list)addAtHead(val int){
newNode := newListNode(val)
newNode.next = l.head.next
l.head.next = newNode
l.length++
}
// 尾插
func (l *list)addAtTail(val int){
cur := l.head
for cur.next != nil{
cur = cur.next
}
newNode := newListNode(val)
cur.next = newNode
l.length++
}
// 任意位置插入
func (l *list)addAtIndex(val,index int){
if index < 0 || index > l.length{
return
}
pre,cur :=l.head ,l.head.next
for i:=0;i<index;i++{
pre = cur
cur = cur.next
}
newNode := newListNode(val)
newNode.next = cur
pre.next = newNode
l.length++
}
// 打印链表
func (l *list)printList(){
cur := l.head.next
for cur != nil{
fmt.Printf("%v ",cur.val)
cur = cur.next
}
fmt.Printf("\n")
}
/*--------------------------删除-----------------------------------*/
// 任意位置删除
func (l *list)deleteListNodeByIndex(index int){
if index < 0 || index > l.length-1{
return
}
pre,cur := l.head,l.head.next
for i:=0;i<index;i++{
pre = cur
cur = cur.next
}
pre.next = cur.next
cur.next = nil
l.length--
}
// 按照指定元素删除
func (l *list)deleteListNodeByVal(val int){
pre,cur := l.head,l.head.next
for cur != nil{
if cur.val == val{
pre.next = cur.next
cur.next = nil
l.length--
}
pre = cur
cur = cur.next
}
}
/*--------------------------查找-----------------------------------*/
// 根据索引查找
func (l *list)getByIndex(index int)int{
if index < 0 || index > l.length-1{
return -1
}
cur := l.head.next
for i:=0;i<index;i++{
cur = cur.next
}
return cur.val
}
问题:206. 反转链表
解题思路
解决方案
// 注意:这里反转链表带着一个无用头节点
func (l *list)reverseList() {
var pre *listNode
cur := l.head.next
for cur != nil{
next := cur.next
cur.next = pre
pre = cur
cur = next
}
l.head.next = pre // 实际上,翻转链表用不上头节点,只是由于前面定义的链表都带上了头节点,为了统一代码,这里多余了一步操作头节点
}
问题:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题24. 两两交换链表中的节点
问题分析
解决方案
func (l *list)swapPairs(){
temp := l.head // temp作为指示器,当指示器后面满足有两个节点时,将这两个节点交换
for temp.next != nil && temp.next.next != nil{
node1 := temp.next
node2 := temp.next.next
temp.next = node2
node1.next = node2.next
node2.next = node1
temp = node1
}
}
练习题:
问题:删除链表的倒数第n个节点,要求只能遍历链表一次。
问题分析
解决方案
func (l *list)removeNthFromEnd(n int){
s,f := l.head,l.head
// 设置快指针的起始位置
for i:= 0;i<=n;i++{
f = f.next
}
// 快慢指针同时移动
for f != nil{
s = s.next
f = f.next
}
// 快指针遍历完链表时,慢指针刚好走到倒数第n个节点的前一个结点
next := s.next
s.next = next.next
next.next = nil
}
问题:给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null
面试题 02.07. 链表相交
问题分析
解决方案
func getIntersectionNode(lA, lB *list) *listNode{
if lA.length > lB.length{
diff := lA.length - lB.length
curA := lA.head.next
curB := lB.head.next
for i:=0;i<diff;i++{
curA := curA.next
}
for curA != nil || curB != nil{
if curA == curB{
return curB
}
curA = curA.next
curB = curB.next
}
}else{
diff := lB.length - lA.length
curA := lA.head.next
curB := lB.head.next
for i:=0;i<diff;i++{
curB := curB.next
}
for curA != nil || curB != nil{
if curA == curB{
return curA
}
curA = curA.next
curB = curB.next
}
}
}
问题:证明一个链表是否成环,如果成环,返回入环点142. 环形链表 II
问题分析
解决方案
func (l *list)detectCycle(head *list) *listNode {
slow, fast := l.head.next, l.head.next
for fast != nil {
slow = slow.Next
if fast.Next == nil {
return nil
}
fast = fast.Next.Next
if fast == slow {
p := l.head.next
for p != slow {
p = p.Next
slow = slow.Next
}
return p
}
}
return nil
}
问题:705. 设计哈希集合 - 力扣(Leetcode)
分析
哈希表首先需要有一个哈希函数,哈希函数负责将key值映射到哈希集合中去
但是,不同的key可能会hash到相同的哈希值,如果此时直接将value写到对应位置,就会覆盖掉另一个key的值,这就是哈希碰撞。解决哈希碰撞的方法有多种
这里使用链地址法解决哈希冲突,构建一个哈希集合
方案(这里没有完全按照leetcode上的题目来设计,如果直接复制代码是跑不通的)
// entry 是哈希集合中实际存放的数据结构
type entry struct{
key interface{}
value interface{}
}
type hashFunc func(key interface{})int
type Hash struct{
hash hashFunc
bucket []*list.List // 用于存放数据的空间,这里称之为桶
}
func NewHash(base int,hash hashFunc)*Hash{
return &Hash{
hash:hash,
bucket:make([]*list.List,base)
}
}
func (h *Hash)Add(key,value interface{}){
if !h.Contain(key){ // 哈希集合是不允许有相同的key的,先判断哈希集合中是否有key
hashKey := h.hash(key) // 先获取key的映射地址
if h.bucket[hashKey] == nil{
h.bucket[hashKey] = list.New()
}
entry := &entry{
key:key,
value:value,
}
h.bucket[hashkey].PushBack(entry)
}
}
func (h *Hash)Contain(key interface{})bool{
hashKey := h.hash(key)
if h.bucket[hashKey] != nil{
for e := h.bucket[hashKey].Front();e != nil;e = e.Next(){
if e.Value.(*entry).key == key{
return true
}
}
}
return false
}
func (h *Hash)Get(key interface{})interface{}{
hashKey := h.hash(key)
if h.bucket[hashKey] == nil || h.bucket[hashKey].Len() == 0{
return -1
}
for e := h.bucket[hashKey].Front();e != nil ;e = e.Next(){
if e.Value.(*entry).key == key{
return e.Value.(*entry).value
}
}
return -1
}
func (h *Hash)Remove(key interface{}){
if !h.Contain(key){
return
}
hashKey := h.hash(key)
for e:= h.bucket[hashKey].Front();e != nil;e = e.Next(){
if e.Value.(*entry).key == key{
h.bucket[hashKey].Remove(e)
}
}
}
问题:460. LFU 缓存 - 力扣(Leetcode)
分析
设计方案
// entry为链表中实际存放的数据,之所以要存放key,是为了淘汰节点后能到cacheMap中去删除对应的映射
type entry struct{
key interface {}
value interface {}
cnt int // 记录当前节点的访问次数
}
type LFUCache struct{
capacity int // 最大容量
size int // 当前cache的容量
minCnt int // 当前cache中访问最低的频次,用于配合list定位需要淘汰的节点
cache map[interface{}]*list.Element // 用于记录key与真实节点的地址映射
list map[int]*list.List // 用于存放不同频次的数据
}
func NewLFUCache(cap int)*LFUCache{
return &LFUCache{
capacity:cap,
size:0,
minCnt:0,
cache:make(map[interface{}]*list.Element),
list:make(map[int]*list.List),
}
}
func (this *LFUCache)Get(key interface{})interface{}{
if this.size == 0{
return -1
}
if ele,ok := this.cache[key];ok{
Value := this.list[ele.Value.(*entry).cnt].Remove(ele) // 从当前频次链表中删除节点
if this.minCnt == Value.(*entry).cnt && this.list[Value.(*entry).cnt].Len() == 0{
this.minCnt++
}
Value.(*entry).cnt++
if _,ok := this.list[Value.(*entry).cnt];!ok{
this.list[Value.(*entry).cnt] = list.New()
}
e := this.list[Value.(*entry).cnt].PushFront(Value) // 将节点插入到新频次的链表中,返回新的节点地址
this.cache[key] = e
}
return -1
}
func (this *LFUCache) Put(key,value interface{}){
if this.capacity == 0{
return
}
if ele,ok := this.cache[key];ok{ //key本来就在cache中,操作与GET相似,多了一步修改value的值
Value := this.list[ele.Value.(*entry).cnt].Remove(ele) // 从当前频次链表中删除节点
if this.minCnt == Value.(*entry).cnt && this.list[Value.(*entry).cnt].Len() == 0{
this.minCnt++
}
Value.(*entry).cnt++
Value.(*entry).value = value // 将节点中的value修改为新的value
if _,ok := this.list[Value.(*entry).cnt];!ok{
this.list[Value.(*entry).cnt] = list.New()
}
e := this.list[Value.(*entry).cnt].PushFront(Value) // 将节点插入到新频次的链表中,返回新的节点地址
this.cache[key] = e //将新的节点地址记录到cache中
}else{ // 插入新的数据节点
// 在插入新节点前,先判断当前cache容量是否满了,如果满了就要淘汰
if this.size == this.capacity{
ele := this.list[this.minCnt].Back() // 从访问频次最低的链表尾端淘汰,一定是访问频次最低且最近未访问的数据
Value := this.list[this.minCnt].Remove(ele)
this.cache = delete(this.cache,Value.(*entry).key) // 根据key删除掉淘汰的节点映射
this.size--
}
this.minCnt = 1 //由于有新的节点插入,当前cache最低访问频次必为1
entry := &entry{
key:key,
value:value,
cnt:1,
}
if _,ok := this.list[this.minCnt];!ok{
this.list[this.minCnt] = list.New()
}
e := this.list[this.minCnt].PushFront(entry)
this.cache[key] = e
this.size++
}
}
问题:146. LRU 缓存 - 力扣(Leetcode)
分析
设计方案
type entry struct{
key interface{}
value interface{}
}
type LRUCache struct{
capacity int
size int
cache map[interface{}]*list.Element
list *list.List
}
func NewLRUCache(cap int)*LRUCache{
return LRUCache{
capacity:cap,
size:0,
cache:make(map[interface{}]*list.Element),
list:list.New(),
}
}
func (this *LRUCache)Get(key interface{}){
if this.size == 0{
return -1
}
if ele,ok := this.cache[key];ok{
this.list.MoveToFront(ele)
return ele.Value.(*entry).value
}
return -1
}
func (this *LRUCache)Put(key,value interface{}){
if this.capacity == 0{
return
}
if ele,ok := this.cache[key];ok{
ele.Value.(*entry).value = value
this.list.MoveToFront(ele)
}else{
if this.size == this.capacity{
e := this.list.Back()
Value := this.list.Remove(e)
this.cache = delete(this.cache,Value.(*entry).key)
this.size--
}
entry := &entry{
key:interface{},
value:interface{},
}
e := this.list.PushFront(entry)
this.cache[key] = e
this.size++
}
}
问题:1206. 设计跳表 - 力扣(Leetcode)
分析
跳表的理解:我们知道,当遇到有序无重复的数组时,我们可以使用二分法快速地在数组中插入,或者查询某个数据,这是因为数组是顺序结构,支持通过下标随机查找地原因。相对而言,对于一个有序链表来讲,即使元素有序,我们要查询某个数据时,依然得从链表头顺序遍历查找,那有没有什么办法能让有序链表拥有二分查找地功能呢?简单的做法就是给跳表中的节点添加索引,通过添加多层索引,我们就可以有效降低链表查找的时间复杂度。这就是跳表的原理
跳表的查询
跳表的插入
在跳表插入num之前,首先要找到num应该插入的位置,由于我们的数据只保存在最底层链表,因此我们首先也要通过跳表的查询功能,找到比num小但最接近num的底层节点位置,然后把num插入在该节点的后面
如果只是单纯的在链表底层插入一个节点,那么跳表的结构就显得很多余了,我们在创建节点时,还必须在上层索引中添加对应的索引!那什么时候才添加索引呢?在哪一层添加索引呢?跳表的设计者给出了一个概率公式,即当跳表节点足够多的时候,跳表的底层节点在每一层以P的概率出现的时候,总的时间复杂度是趋于稳定的。
我们假设这个P是1/2,即底层某个节点在第一层索引出现的概率是1/2,在第二层索引出现的概率就是1/2*1/2=1/4。(因为跳表规定,上层索引出现的节点,在下层必须出现)
因此,我们在插入一个节点后,还要根据概率给该节点生成一个索引层数,根据这个索引层数来在上层索引中插入节点。以概率P=1/2为例,我们想象一下抛硬币,正面为0,反面为1.在插入一个节点后,我们开始抛硬币,直到抛出正面为止,记录抛硬币的次数就是我们的索引层数。
综上所述,我们插入节点时首先要找到底层节点的插入位置,然后插入底层节点,再构造上层索引。那么还有一个问题,就是我们在前面已经通过跳表一层一层跳到底层了,又怎么回去上面层次插入索引呢?难道又从头开始跳?当然,我们可以选择空间换时间的方法。
跳表的删除
跳表设计
const MaxLevel = 32 // 跳表的最大层次
const P = 0.5 // 定义节点出现在每一层的概率
type Node struct{
Val int // 存放数据
Forward []*Node //由于每个节点都有可能成为上层索引,我们并不知道某个节点究竟有几层索引,因此就用一个切片来存放指针,Forward[i]表示的是当前节点在第i层指向的下一个节点的地址
}
type SkipList struct{
Head *Node // 虚拟头节点,便于节点的插入删除操作
Level int // 记录当前跳表的最大索引层数
}
func NewSkipList()*SkipList{
return &SkipList{
Head:&Node{
Val:-1,
Forward:make([]*Node,MaxLevel) // 由于不知道每个节点的层次,因此直接开到最大层次,通过Level字段保存最大的层次即可。
},
Level:0,
}
}
func (s *SkipList)Search(target int)bool{
cur := s.Head // 从虚拟头节点开始查找
for i := s.Level-1 ; i >= 0 ; i--{ // 从当前跳表的最上层索引开始查找
for cur.Forward[i] != nil && cur.Forward[i].Val < target{ // 在第i层中找到比target小且最接近target的节点
cur = cur.Forward[i]
}
}
cur = cur.Forward[0] // 从for循环出来时,cur此时一定指向了最接近target的值,我们获取其最底层的下一个节点
return cur != nil && cur.Val == target
}
func (s *SkipList)Add (num int){
update := make([]*Node,MaxLevel) // 用于记录每一层比num小且最接近num的节点,便于后续 插入索引
cur := s.Head
for i := range update{ //这步初始化是必须的,因为新插入的节点层次可能比现有节点层次高,而update保存的是最接近num的节点,当新节点层次比现有层次高时,相当于在最上层再开索引,那么此时最接近num的节点就是头节点
update[i] = s.Head
}
for i := s.Level - 1;i >= 0;i--{
for cur.Forward[i] != nil && cur.Forward[i].Val < num{
cur = cur.Forward[i]
}
update[i] = cur // 记录第i层中比num小但最接近num的节点
}
lv := RandomLevel() // 通过随机算法获取新节点的索引层次
s.Level = max(s.Level,lv) // 如果新节点的层次比跳表最高层还大,说明要再建一层上层索引,因此修改跳表索引层次
newNode := &Node{ // 构造新节点
Val:num,
Forward:make([]*Node,lv) // 只会在第0-第(lv-1)层存在索引
}
for i,node := range update[:lv]{ // 从第0层开始,一直到(lv-1)层,依次插入新节点
newNode.Forward[i] = node.Forward[i]
node.Forward[i] = newNode
}
}
func RandomLevel()int{
lv := 1
for lv < MaxLevel && rand.Float64() < P{
lv++
}
return lv
}
func max (a,b int)int{
if a > b {
return a
}
return b
}
func (s *SkipList) Erase(num int)bool{
update := make([]*Node,MaxLevel)
cur := s.Head
// 注意,这里和Add不一样的地方在于,我们不需要初始化update了,因为我们在删除节点的时候不可能会比原来的层次更小了,如果某一层的update为nil,说明这一层根本不存在节点,更不需要删除节点了
for i:=s.Level-1;i >= 0;i--{
for cur.Forward[i] != nil && cur.Forward[i].Val < num{
cur = cur.Forward[i]
}
update[i] = cur
}
cur = cur.Forward[0]
if cur == nil || cur.Val != num{ // 说明跳表中根本不能存在num
return false
}
// 由于我们不知道删除的num在那一层存在着索引,因此我们从第0层开始,一直到当前跳表的最高层,依次寻找节点num进行删除即可
for i:=0;i<s.Level && update[i].Forward[i] == cur;i++{
update[i].Forward[i] = cur.Forward[i]
}
// 最后,我们来维护一下当前跳表的最大层次
for this.Level > 1 && this.Head.Forward[this.Level-1] == nil{
this.Level--
}
return true
}