基本知识点
节点(Node)
public class Node {
var value: Value // 持有一个值
var next: Node? // 下一个节点
init(value: Value, next: Node? = nil) {
self.value = value
self.next = next
}
}
extension Node: CustomStringConvertible {
public var description: String {
guard let next = next else {
return "\(value)"
}
return "\(value) -> " + String(describing: next) + " "
}
}
创建一个简单列表
let node1 = Node(value: 1)
let node2 = Node(value: 2)
let node3 = Node(value: 3)
node1.next = node2
node2.next = node3
print(node1)
输出:1 -> 2 -> 3
虽然这种方式可以创建链表,但是如果需要长链表的话,那么就变得不实用了。所以,一种普遍的方法就是创建链表来管理节点对象。
创建链表
public struct LinkedList {
var head: Node? //头结点
var tail: Node? //尾结点
init() {}
var isEmpty: Bool { //链表是否为空
return head == nil
}
}
extension LinkedList: CustomStringConvertible {
public var description: String {
guard let head = head else {
return "Empty List"
}
return String(describing: head)
}
}
结构体链表中拥有头尾节点两个属性,链表中的头尾节点是指链表的第一个节点和最后一个节点.
添加值到链表
这里有3种方式,每一种方式都有自己的独自性能特性:
push:在列表的前面添加值,即头插法
append:在列表的末尾添加值,即尾插法
insert(after:):在列表中特定节点的后面添加值
- push
添加一个值到链表的最前面,我们使用push
操作,也叫head-first insertion
,时间复杂度为O(1)
。在LikedList
结构体中添加如下方法:
mutating func push(_ value: Value) {
head = Node(value: value, next: head)
if tail == nil { // 创建新节点,没有尾节点,头节点和尾节点指向同一个新节点
tail = head
}
}
如果我们添加一个节点到一个空链表,那么头尾节点都指向改新节点。简单使用一下:
var list = LinkedList()
list.push(4)
list.push(5)
list.push(6)
print(list) //输出结果:6 -> 5 -> 4
- append
拼接操作,即在链表的末尾添加一个值,也叫tail-end insertion
,时间复杂度为O(1)。在LikedList
结构体中添加如下方法:
mutating func append(_ value: Value) {
// 1
guard !isEmpty else {
push(value)
return
}
// 2
tail?.next = Node(value: value)
// 3
tail = tail?.next
}
代码很简单
1)判断链表是否为空,为空即执行push
操作创建一个新的节点。
2)创建一个新节点,赋值为尾部节点的下一个节点,即将节点连接起来
3)因为是尾部拼接节点,所以新的节点将成为尾部节点
简单使用
var list = LinkedList()
list.append(1)
list.append(2)
list.append(3)
print(list) // 1 -> 2 -> 3
- insert(after:)
插入一个值到链表的具体位置,需要二步:
1)找到链表中具体的节点
func node(at index: Int) -> Node? {
//1
var currentNode = head
var currentIndex = 0
//2
while currentNode != nil && currentIndex < index {
currentNode = currentNode?.next
currentIndex += 1
}
return currentNode
}
上面代码是根据给定的位置,获取对应位置的节点,因为我们只能从列表的头节点开始访问,所以使用while循环遍历获取对应位置的节点,链表为空或者越界返回nil。
2)插入一个新的节点
//1 discardableResult关键字告诉调用者可以忽略返回值
@discardableResult
public mutating func insert(_ value: Value, after node: Node) -> Node {
//2 判断插入的结点是否是尾结点,如果是尾结点,那么直接尾部拼接结点
guard tail !== node else {
append(value)
return tail!
}
//3 创建新的结点
//1)把当前结点的下一个结点作为新结点的下一个结点
//2)并把新结点作为当前结点的下一个结点
node.next = Node(value: value, next: node.next)
return node.next!
}
简单使用
var list = LinkedList()
list.push(3)
list.push(2)
list.push(1)
print("Before inserting: \(list)")
var middleNode = list.node(at: 1)!
for _ in 1...4 {
middleNode = list.insert(-1, after: middleNode)
}
print("After inserting: \(list)")
运行输出
Before inserting: 1 ->2 ->3
After inserting: 1 ->2 ->-1 ->-1 ->-1 ->-1 ->3
从链表中去除结点
有3种主要的方式去除结点:
pop:从链表的开始去除结点
removeLast:从链表的最后去除结点
remove(at:):从链表的指定位置去除结点
- pop
public mutating func pop() -> Value? {
defer { // defer关键字表示延迟执行
head = head?.next // 头结点的下一个结点作为头结点
if isEmpty { // 如果链表为空,清空尾结点
tail = nil
}
}
return head?.value // 返回被去除的结点值
}
简单删除一个结点,时间复杂度为O(1)
简单使用
var list = LinkedList()
list.push(3)
list.push(2)
list.push(1)
print("before popping list: \(list)")
let poppedValue = list.pop()
print("after popping list: \(list)")
print("popped value: " + String(describing: poppedValue))
运行输出
before popping list: 1 ->2 ->3
after popping list: 2 ->3
popped value: Optional(1)
- removeLast
@discardableResult
public mutating func removeLast() -> Value? {
// 1 如果头结点尾nil,直接返回nil
guard let head = head else {
return nil
}
// 2 头结点的下一个结点存在继续执行,不存在说明只有一个结点,等同于执行pop操作
guard head.next != nil else {
return pop()
}
// 3 引用头结点进行遍历查找最后结点
var prev = head
var current = head
while let next = current.next { // 当前结点的下一个结点是否存在
prev = current
current = next
}
// 4 将最后结点去除,因为pre是最后结点的上一个结点
prev.next = nil
tail = prev // 更新尾结点
return current.value
}
因为需要遍历整个链表,所以是相对耗时的,时间复杂度为O(n)
简单使用
var list = LinkedList()
list.push(3)
list.push(2)
list.push(1)
print("before removing last node: \(list)")
let removedValue = list.removeLast()
print("after removing lasr node: \(list)")
print("removed value: " + String(describing: removedValue))
运行输出
before removing last node: 1 ->2 ->3
after removing lasr node: 1 ->2
removed value: Optional(3)
- remove(after:)
去除指定位置的结点操作。其实跟之前的插入结点到指定位置是类似的,需要先找到希望去除的结点,然后去除
@discardableResult
public mutating func remove(after node: Node) -> Value? {
defer {
if node.next === tail { // 如果该结点的下一个结点是尾结点
tail = node // 当前结点设置为尾结点
}
node.next = node.next?.next // 去除node结点之后的结点
}
return node.next?.value // 返回node结点之后结点的值
}
简单使用
var list = LinkedList()
list.push(3)
list.push(2)
list.push(1)
print("before removing at particular index: \(list)")
let index = 1
let node = list.node(at: index - 1)!
let removedValue = list.remove(after: node)
print("after removing at index \(index): \(list)")
print("removed value: " + String(describing: removedValue))
运行输出
before removing at particular index: 1 ->2 ->3
after removing at index 1: 1 ->3
removed value: Optional(2)
相关题
1、给出一个链表和一个值x,要求将链表中所有小于x的值放到左边,所有大于或等于x的值放到右边,并且原链表的结点顺序不能变。
例如:1->5->3->2->4->2,给定x=3,那么返回 1->2->2->5->3->4
思路:根据题目的意思,我们只需要遍历链表,然后将小于给定值的结点组成一个链表,大于给定值的结点组成一个链表,最后两个链表进行拼接即可。
实现
结点
class Node {
var value: Int
var next: Node?
init(_ value: Int) {
self.value = value
self.next = nil
}
}
extension ListNode: CustomStringConvertible {
public var description: String {
guard let next = next else {
return "\(value)"
}
return "\(value) ->" + String(describing: next) + " "
}
}
链表
class List {
var head: ListNode?
var tail: ListNode?
// 尾插法
func appendToTail(_ value: Int) {
if tail == nil {
tail = ListNode(value)
head = tail
} else {
tail!.next = ListNode(value)
tail = tail!.next
}
}
// 头插法
func appendToHead(_ value: Int) {
if head == nil {
head = ListNode(value)
tail = head
} else {
let node = ListNode(value)
node.next = head // 当前结点的下一个结点为头结点
head = node // 头结点指向当前结点
}
}
}
在List中添加功能实现函数
func listSort(_ head: ListNode? , _ x : Int) -> ListNode? {
let leftDummy = ListNode(0), rightDummy = ListNode(0) //1
var left = leftDummy, right = rightDummy
var node = head
// 用尾插法处理左边和右边
while node != nil { //2
if node!.value < x { //3
left.next = node
left = node!
} else { //4
right.next = node
right = node!
}
node = node!.next //5
}
right.next = nil
// 左右拼接
left.next = rightDummy.next
// 返回假结点的下一个结点开始,即新链表
return leftDummy.next
}
1、创建两个假的结点,方便用于连接给定值左右两边的结点
2、判断当前结点是否存在,如果存在进行循环,获取结点值与给定值进行比较
3、如果当前结点值小于给定值就拼接到左结点之后,并移动指针指向末尾结点,方便后续继续添加结点
4、如果当前结点值大于给定值就拼接到右结点之后,并移动指针指向末尾结点
5、获取链表头结点的下一个结点继续循环,一直到链表末尾
使用
let list = List()
list.appendToTail(1)
list.appendToTail(5)
list.appendToTail(3)
list.appendToTail(2)
list.appendToTail(4)
list.appendToTail(2)
print(list.head!) //1 ->5 ->3 ->2 ->4 ->2
let newList = list.listSort(list.head, 3)
print(newList!) // 1 ->2 ->2 ->5 ->3 ->4
2、快行指针
快行指针就是两个指针访问链表,一个在前,一个在后,或者一个移动快,另一个移动慢。在处理链表的时候,我们经常用到两个指针遍历链表:previous 和 current,也就是current 比 previous 超前一个节点
1)如何检测一个链表中是否有环?
思路:使用两个指针同时访问链表,其中一个的速度是另一个的两倍,如果它们变成相等,那么这个链表就有环了。
func hasCycle(_ head: ListNode?) -> Bool {
var slow = head
var fast = head
while fast != nil && fast!.next != nil {
slow = slow!.next
fast = fast!.next!.next
}
if slow === fast {
return true
}
return false
}
2)删除链表中倒数第n个结点。例如:1->2->3->4->5,n=2,返回 1->2->3->5
注意:给定的n长度小于等于链表的长度
思路:两个指针的移动速度相同,但是一开始,第一个指针(在指向头结点之前)就落后第二个指针n个结点。接着两者同时移动,当第二个指针移动到为结点时,第一个结点的下一个结点就是我们要删除的结点。
func removeNode(head: ListNode?, _ n: Int) -> ListNode? {
guard let head = head else { return nil } // 链表为空,返回nil
// 创建一个虚拟结点,头结点作为下一个结点
let dummy = ListNode(0)
dummy.next = head
// 前后两个结点指向同一个结点
var previuos: ListNode? = dummy
var current: ListNode? = dummy
// 设置后一个结点的初始位置,即移动n个位置
for _ in 0..