重学数据结构与算法(4)-链表的一些常用方法

226 阅读3分钟

这是我参与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
	}
}

总结

自己实现了一个删除链表最后一个节点的例子,其实链表还有很多方法,例如删除指定值的节点,指定位置的节点,判断某个值是否在链表中,都是面试经常要用到,我们慢慢来,先简单的开始,下面准备对比一下链表和数组的区别。