• 快速排序与归并排序的链式实现(golang)


    前言

    众所周知 快速排序 是一种平均时间性能在 O ( n ⋅ l o g ( n ) ) O(n\cdot log(n)) O(nlog(n))的算法。归并排序 最坏情况性能和平均时间性能都在 O ( n ⋅ l o g ( n ) ) O(n\cdot log(n)) O(nlog(n))。因为思想简单,性能优秀,十分常见。

    即使思想简单,但实现依然十分不易。主要原因大概是因为大多采用数组进行实现,为了降低内存开销,会复用已有的数组,从而使空间复杂度降至 O ( 1 ) O(1) O(1)。为了达到这一目的,人们采用一些奇技淫巧,让代码理解、实现都变容易出错。

    但是链表结点天然具有可复用性,如果能用链表来实现这两种算法,不仅能达到省内存的目的,而且去掉 奇技淫巧后的代码结构更接近于算法的本质思想,易于理解和在实际项目中手动实现。

    本文不再赘述这两种算法的思想和基于数组的实现方式。仅提供基于链表的实现方式的介绍。

    链表结构

    链表结点结构(以整数型为例)如下。

    type ListNode struct {
    	data int       // 链表结点的数据
    	next *ListNode // 下一个结点的指针
    }
    
    type List *ListNode
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    快速排序

    算法首先把链表拆分成sublist和pivot两个子链表,初始时这两个链表都为空。
    其中pivot结点中的值就是原链表首元结点的值。
    另外有一个指针node指向原链表第二个结点

    在这里插入图片描述
    然后从node开即往后遍历,如果某个结点的值比pivot中的值小,则把该节点挂在sublist链表中。否则挂在pivot链表中。如下图,3因为比7小,所以挂在sublist,8因为比7大所有挂在pivot链表。
    在这里插入图片描述
    遍历完成后,分别对这个两子链表进行排序。直接递归调用即可。排序后如下图所示。
    在这里插入图片描述
    最后把这两个子链表合并即可

    在这里插入图片描述

    平均时间复杂度: O ( n ⋅ l o g ( n ) ) O(n\cdot log(n)) O(nlog(n))

    空间复杂度: O ( 1 ) O(1) O(1)

    代码如下:

    func QuickSort(list List) {
    	// 如果链表没有元素或只有一个元素,那么不用排序
    	if list.next == nil || list.next.next == nil {
    		return
    	}
    
    	// 指定第一个元素为pivot, 第二个元素为node
    	node := list.next.next
    	pivot := list.next
    	pivot.next = nil
    	sublist := list
    	sublist.next = nil
    
    	// 把其它元素根据大小分别放在pivot的左边和右边。 
    	// 原链表将拆分成以sublist和pivot为头结点的两个链表
    	for node != nil {
    		nodeNext := node.next
    		if node.data < pivot.data {
    			node.next = sublist.next
    			sublist.next = node
    		} else {
    			node.next = pivot.next
    			pivot.next = node
    		}
    		node = nodeNext
    	}
    
    	// 递归对两个子链表进行排序
    	QuickSort(sublist)
    	QuickSort(pivot)
    
    	// 合并两个链表
    	i := list
    	for i.next != nil {
    		i = i.next
    	}
    	i.next = pivot
    }
    
    • 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

    归并排序

    在链表上实现归并排序关键在于如何快速地找到分割点将链表分割成两个子链表。本文提供的方法只需要扫描链表一遍即可确定分割点(如代码所示)。

    算法首先找到分割点midNode, 该结点及其之前的结点被分在list1中,之后的结点被分在list2中。
    之后递归地对这两个子链表进行排序。如下图。
    在这里插入图片描述

    最后,把排序后的list1和list2归并即可。归并过程如下,详细如图:

    • 归并过程首先会把list1和list2指向两个子链表的第一个节点。把list链表置空。
    • 然后,依次摘取两个子链表中具有较小数值的结点放入list链中。
    • 最后得到的list就是归并完成的结果。
      在这里插入图片描述
      平均时间复杂度: O ( n ⋅ l o g ( n ) ) O(n\cdot log(n)) O(nlog(n))
      最好和最坏性况时间复杂度: O ( n ⋅ l o g ( n ) ) O(n\cdot log(n)) O(nlog(n))
      空间复杂度: O ( 1 ) O(1) O(1)
    func MergeSort(list List) {
    	// 如果链表没有元素或只有一个元素,那么不用排序
    	if list.next == nil || list.next.next == nil {
    		return
    	}
    
    	// 计算链表长度, 并找到分割点
    	midNode := list
    	shouldNext := true
    	for i := list.next; i != nil; i = i.next {
    		if shouldNext {
    			midNode = midNode.next
    		}
    		shouldNext = !shouldNext
    	}
    
    	// 把链表分成两个子链表
    	list1 := list
    	list2 := &ListNode{next: midNode.next}
    	midNode.next = nil
    
    	// 分别对两个子链表递归处理
    	MergeSort(list1)
    	MergeSort(list2)
    
    	// 归并且合并两个子链表
    	list1 = list1.next
    	list2 = list2.next
    	tail := list
    	tail.next = nil
    	for list1 != nil && list2 != nil {
    		if list1.data < list2.data {
    			tail.next = list1
    			list1 = list1.next
    			tail = tail.next
    		} else {
    			tail.next = list2
    			list2 = list2.next
    			tail = tail.next
    		}
    	}
    
    	for list1 != nil {
    		tail.next = list1
    		list1 = list1.next
    		tail = tail.next
    	}
    
    	for list2 != nil {
    		tail.next = list2
    		list2 = list2.next
    		tail = tail.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
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    测试

    本文把两个算法排序的结果与go语言自带的排序功能进行比较,验证了算法实现的正确性。

    func main() {
    	list1 := new(ListNode)
    	list2 := new(ListNode)
    	array := make([]int, 100)
    	// 随机生成100个数
    	rand.Seed(time.Now().UnixMicro())
    	for i := 0; i < 100; i++ {
    		number := rand.Intn(100)
    		list1.next = &ListNode{data: number, next: list1.next}
    		list2.next = &ListNode{data: number, next: list2.next}
    		array[i] = number
    	}
    
    	QuickSort(list1)
    	MergeSort(list2)
    	sort.Ints(array)
    
    	// 输出, 并验证排序是否正确
    	i1 := list1.next
    	i2 := list2.next
    	j := 0
    	for i1 != nil {
    		if i1.data != array[j] {
    			panic("list1 not equal")
    		}
    		if i2.data != array[j] {
    			panic("list2 not equal")
    		}
    		fmt.Print(i1.data, ",")
    		i1 = i1.next
    		i2 = i2.next
    		j++
    	}
    	fmt.Println()
    }
    
    
    • 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

    测试结果:

    在这里插入图片描述

  • 相关阅读:
    Spring框架系列(14) - SpringMVC实现原理之DispatcherServlet处理请求的过程
    Docker部署Logstash 7.2.0
    Vue前后端交互、生命周期、组件化开发
    什么是浏览器指纹?指纹浏览器如何避免浏览器指纹的追踪识别?
    WCH USB转多串口芯片相关型号
    【Java刷题进阶】基础入门篇⑤
    asp.net core在其他程序集获取HttpContext
    凉鞋的 Godot 笔记 201. 第三轮循环:引入变量
    【应用层协议】初始Http,fiddler的使用
    Git 的概念以及相关操作
  • 原文地址:https://blog.csdn.net/u013749051/article/details/126832107