这是我参与8月更文挑战的第4,活动详情查看:8月更文挑战
回顾
上篇使用动态数组实现了一个堆栈。动态数组有一个明显的缺点就是会造成明显的内存空间浪费,例如Golang中切片操作底层数组的时候,在数组容量不够的情况下会以原数组占用的内存空间的1.25倍进行扩容。有时候我只添加一个元素的话,这个元素就会占用原数组占用的内存空间的四分之一的内存。
链表
为了解决动态数组浪费内存的缺点,链表就可以站出来了,可以用多少内存就申请多少内存。链表是一种链式的储存结构,其中的所有的元素的内存地址不一定是连续的。但是链表有三种链表:单向链表,双向链表,循环链表。我们先从简单的单向链表开始学习,因为单向链表是最常见的。
单向链表的方法
单向链表的一些方法是和动态数组是一样的,单向链表的节点结构基本上有两个部分,即数据字段和指针组成,指针指向的是下一个元素在内存中的位置 ,链表也是一个顺序的储存结构,但是其中的元素的内存地址不一定是顺序的,我们同样来用Golang来实现一个链表结构吧:
package main
import (
"errors"
"fmt"
)
//定义节点
type Node struct {
Data string //节点储存的类型为string
NextData *Node //存放下个节点的指针
}
//定义单向链表结构
type LinkedList struct {
Head *Node //头节点指向实际的栈顶
}
func makeLinkedList() *LinkedList {
return &LinkedList{}
}
//判断链表是否为空
func (l *LinkedList) isEmpty() bool {
return l.Head == nil
}
//返回栈顶元素
func (l *LinkedList) top() interface{} {
if l.isEmpty() { //说明栈为空
return errors.New("栈为空")
}
return l.Head.Data //栈顶元素
}
//入栈
func (l *LinkedList) push(value string) {
newNode := &Node{Data: value} //入栈的值创建一个新的节点
if l.isEmpty() {
l.Head = newNode //为空讲head头指向这个最新的
} else {
//不为空
cur := l.Head
//循环遍历,直到遍历到最后一个节点,往最后节点的存放下个节点的指针为空的时候,把这个指针指向新加的节点
for cur.NextData != nil {
cur = cur.NextData
}
//更新新加节点
cur.NextData = newNode
}
}
//出栈,删除尾部元素
func (l *LinkedList) pop() interface{} {
cur := l.Head //获取头节点
if cur.NextData == nil {
return nil
} else {
for cur.NextData == nil { //如果下个节点为空,那么当前的节点的值就是下下个节点的值,进行链表位移
cur = cur.NextData.NextData
}
cur.NextData.NextData = nil //删掉倒数第二个节点指最后一个节点的指针
}
return nil
}
func main() {
list := makeLinkedList()
list.push("hello~")
list.push("world~")
list.push("我是土味挖掘机~")
//遍历几点
cur := list.Head
for cur != nil {
fmt.Println(cur.Data)
cur = cur.NextData
}
fmt.Printf("当前head节点值:%s\n", list.top())
list.pop()
cur = list.Head
for cur != nil {
fmt.Println(cur.Data)
cur = cur.NextData
}
}
总结
自己实现了一个删除链表最后一个节点的例子,其实链表还有很多方法,例如删除指定值的节点,指定位置的节点,判断某个值是否在链表中,都是面试经常要用到,我们慢慢来,先简单的开始,下面准备对比一下链表和数组的区别。