经典排序算法和pdqsort工程排序算法 | 青训营笔记

96 阅读4分钟

这是我参与「第五届青训营」伴学笔记创作活动的第12天。 本次课程的内容包括经典排序算法,如插入排序,快速排序,堆排序。然后讲解了go语言最佳算法pdqsort,并比较了他们的性能。

一 经典排序算法

(一 插入排序

image.png

如图所示,插入排序是一种简单直观的排序算法。它的工作原理是通过先构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

算法描述:

  1. 把待排序的数组分成已排序和未排序两部分,初始的时候把第一个元素认为是已排好序的。
  2. 从第二个元素开始,从后往前在已排好序的子数组中寻找到该元素合适的位置并插入该位置。
  3. 重复上述过程直到最后一个元素被插入有序子数组中。

图中示例:

数组里起始只有一个元素 5 ,其本身算是一个有序序列,将后续元素从后往前比较,直到插入已经排序好的 array 中,即不断交换,直到找到第一个比其小的元素。重复上述过程直到所有元素插入完成。

代码实现:

func insert_sort(li []int) {
    for i := 1; i < len(li); i++ {
        tmp := li[i]
        j := i - 1
        for j >= 0 && tmp < li[j] {
            li[j+1] = li[j]
            j --
        }
        li[j+1] = tmp
    }
}

(二 快速排序

快速排序是一个知名度极高的排序算法,其对于大数据的优秀排序性能和相同复杂度算法中相对简单的实现使它注定得到比其他算法更多的宠爱。

image.png

算法描述:

  • 1、从数列中挑出一个元素,称为"基准",一般取最左边或最右边元素。
  • 2、重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区操作。这个时候基准左边为左分区,基准右边为右分区。
  • 3、递归把小于基准值元素的左分区数列和大于基准值元素的右分区数列按上述方法排序。

图中示例:

  • 给出数列
  • 第一轮:先选择最右边的4为基准点,从后往前遍历,比4大的元素放左边,比4小的元素放右边,等于4的元素放中间。
  • 第二轮:中间不动。左分区选择2基准,没有比2小的,比2大的放右边。右分区选择8为基准,没有比8大的,比8小的放左边。
  • 第三轮:右边无序,继续选择5为基准,没有比5小的,比5大的放右边.
  • 完成排序

算法代码:

func quickSort(data []int){
	if len(data) < 1{
		return
	}
	l,r := 0,len(data) - 1
	//基准值
	base := data[0]
	for i := 0;i <= r;{
		//比基准值大的放右边
		if data[i] > base{
			data[r],data[i] = data[i],data[r]
			r--
		}else{
			//比基准值小或等于的放左边
			data[l],data[i] = data[i],data[l]
			l++
			i++
		}
	}
	quickSort(data[:l])
	quickSort(data[l + 1:])
}

(三 堆排序

1、什么是堆:

堆是一种特殊的树,它满足需要满足两个条件:

(1)堆是一种完全二叉树,也就是除了最后一层,其他层的节点个数都是满的,最后一个节点都靠左排列。

(2)堆中每一个节点的值都必须大于等于(或小于等于)其左右子节点的值。

对于每个节点的值都大于等于子树中每个节点值的堆,我们叫作“大顶堆”。对于每个节点的值都小于等于子树中每个节点值的堆,我们叫作“小顶堆”。

2、堆的实现:

用数组来存储完全二叉树是非常节省内存空间的,因为我们不需要存储左右子节点的指针,单纯通过数组的下标,就可以找到一个节点的子节点和父节点。

从图中我们可以看到,数组中下标为 i 的节点的左子节点,就是下标为 i∗2+1 的节点,右子节点就是下标为 i∗2+2的节点。

image.png 8b8f63a80fc63bb9f1c294908ac2008d_9e9b5f5965884d02b5e863b58788c5ff.png

3、堆排序流程

  1. 创建一个堆H[0……n-1],可能无序;
  2. 如下图所示,从下到上比较子节点和父节点的大小,如果子节点比父节点大,则交换位置,直到构造成一个大顶堆。 93d17eebab57507d8c41aeaedcb39b98_v2-f4c5c8713818b665e69b9ef2e8313d31_720w.webp
  3. 把堆首(最大值)取出;
  4. 把堆的尺寸缩小 1,把新的数组顶端数据调整到相应位置;
  5. 重复步骤 2,3,直到堆的尺寸为 1。

4、下面是完整的堆排序流程: e00305d2e83f25a6e9fee5072287d674_v2-ca9f1d789756738a8bab9a54251716b0_720w.webp

如图所示,

  1. a为第一次创建的大顶堆,然后取出堆首16。
  2. 堆尺寸-1,从上到下比较子节点和父节点的大小,如果子节点比父节点大,则交换位置,构造新的大顶堆,然后取出堆首
  3. 重复上述过程。

5、代码实现:

func sift(li []int, low, high int) {
    i := low
    j := 2*i + 1
    tmp:=li[i]
    for j <= high {
        if j < high && li[j] < li[j+1] {
            j++
        }
        if tmp < li[j] {
            li[i]  = li[j]
            i = j
            j = 2*i + 1
        } else {
            break
        }
    }
    li[i] = tmp
}

func heap_sort(li []int) {
    for i := len(li)/2 - 1; i >= 0; i-- {
        sift(li, i, len(li)-1)
    }
    for j := len(li) - 1; j > 0; j-- {
        li[0], li[j] = li[j], li[0]
        sift(li, 0, j-1)
    }
}

几个排序方法的复杂度和性能总结:

image.png

pdqsort排序算法

特点

pdqsort是一种支持切换排序算法的,结合三种排序方法的优点的算法:

  1. 当数列长度小于某一特定值时,我们优先使用插入排序来保证数据的组织;
  2. 此外,当数据量较大时,则使用快速排序以实现最优性能,
  3. 而在快速排序表现不佳时,则应采用堆排序来确保最坏情况下时间复杂度仍然为 O(n*logn) 。

所以pdqsort的切换机制如下:

  1. 对于短序列 (<=24) 我们使用插入排序.
  2. 其他情况,使用快速排序(选择首个元素作为 pivot) 来保证整体性能
  3. 然而,当快速排序表现不佳时(limit==0),则采用堆排序确保最坏情况下时间复杂度仍然为 O(n*logn) 。

算法代码 V2

如果大家想看一下该算法的具体实现,可以查看https://github.com/zhangyunhao116/pdqsort中的代码

pdqsort和其他几个排序方法的复杂度和性能比较:

image.png