几个常用排序的代码实现堆排序|快排|归并排序 Marathon

以下内容来自网络,如侵权请联系我

堆排序|快排|归并排序

  • 三种排序算法的时间复杂度:O(nlogn)

  • 三种排序算法的空间复杂度:

    • 快排:平均O(logn), 最坏O(n)
    • 归并:O(n)
    • 堆排:O(1),topK问题可能O(k)
  • 运行时间:快排 < 归并排序 < 堆排序

  • 算法特点:

    • 快排:不稳定,极端情况排序效率低,最坏会退化成O(n^2)
    • 归并排序:稳定,需要额外的内存空间开销
    • 堆排序:不稳定,在效率较高的排序算法中,相对较慢

1.快速排序

python

# 快速排序

def partition(nums: [int], left: int, right: int):
    """分区函数,左边比中间小,右边比中间大"""
    if left > right:
        return nums
    pivot = left
    i, j = left, right
    while i < j:  # while循环的操作在于寻找pivot位置并交换元素,最终使得pivot为基准,分割左右半区元素,继续递归
        while i < j and nums[j] >= nums[pivot]:  # 大于pivot对应元素时, j--,实现右指针左移,直到 j <= i(往往相遇时相等) 或者 nums[j] < nums[pivot]
            j -= 1
        while i < j and nums[i] < nums[pivot]:  # 小于等于pivot对应元素时,i++,实现左指针右移,直到 i >= j(往往相遇时相等) 或者 num[i] > nums[pivot]
            i += 1
        nums[i], nums[j] = nums[j], nums[i]  # 当 i=j 时,已经跳出上面两个while循环,即左右指针相遇,或者(j的元素小于等于pivot && i的元素大于pivot),肯定需要交换不同位置元素,实现pivot分割数组,最终使得左右指针相遇
    nums[pivot], nums[j] = nums[j], nums[pivot]  # 交换基准与左右指针相遇位置的元素,此次分区结束,新排序的数组一定是pivot元素大于左区,小于右区
    partition(nums, left, j - 1)  # 递归左半区
    partition(nums, j + 1, right)  # 递归右半区
    return nums

def quickSort(nums: [int]) -> [int]:
    """快速排序"""
    length = len(nums)
    return partition(nums, 0, length-1)


if __name__ == "__main__":
    import random
    nums = list(range(50))
    random.shuffle(nums)
    randomNums = nums[:10]
    print(randomNums)
    print(quickSort(randomNums))

golang

func quickSort(nums []int) []int {
	// 快速排序
	length := len(nums)
	return partition(nums, 0, length-1)
}

func partition(nums []int, left, right int) []int {
	// 分区及排序,快排的具体实现
	// 递归终止条件
	if left > right {return nil}
	// 设置基准-哨兵
	pivot := left
	i, j := left, right
	// 循环遍历,直到 i==j
	for i < j {
		for i < j && nums[j] >= nums[pivot] {
			j--
		}
		for i < j && nums[i] <= nums[pivot] {
			i++
		}
		// 交换元素
		nums[i], nums[j] = nums[j], nums[i]
	}
	// 交换基准与此轮相遇位置元素
	nums[pivot], nums[j] = nums[j], nums[pivot]
	// 递归左区
	partition(nums, left, i-1)
	// 递归右区
	partition(nums, i+1, right)
	return nums
}

2.归并排序

Python内部的sort()方法是基于归并排序实现的。

# 归并排序

def merge(arr: [int], start: int, mid: int, end: int):
    i = start
    j = mid + 1
    tmpArr = [] # 开辟临时容器
    while i <= mid and j <= end: # 当左右两个分区都有数据时
        if arr[i] < arr[j]: # 左边小,加入tmpArr
            tmpArr.append(arr[i])
            i += 1
        else:
            tmpArr.append(arr[j]) # 右边小,加入tmpArr
            j += 1
    # 上面while完成后,左右两区有一区没数据,另外一区直接添加到尾部
    while i <= mid:
        tmpArr.append(arr[i])
        i += 1
    while j <= end:
        tmpArr.append(arr[j])
        j += 1
    arr[start:end+1] = tmpArr # tmpArr按arr原位置写入

def mergeSort(arr: [int], start: int, end: int) -> [int]:
    if start < end:
        mid = (start+end) >> 1
        # 递归左边
        mergeSort(arr, start, mid)
        # 递归右边
        mergeSort(arr, mid+1, end)
        # 合并数据
        merge(arr, start, mid, end)

    return arr

if __name__ == "__main__":
    import random
    nums = list(range(100))
    random.shuffle(nums)
    nums = nums[:20]
    randomNums = [60, 54, 62, 49, 24, 13, 88, 59, 66, 57, 42, 23, 0, 15, 8, 67, 21, 9, 12, 78]
    print(mergeSort(randomNums, 0, 19))

golang实现

// mergeSort
func mergeSort(nums []int) []int {
	// 递归终止
	if len(nums) <= 1 {
		return nums
	}
	// 拆分与合并
	mid := len(nums) >> 1
	left := mergeSort(nums[:mid])
	right := mergeSort(nums[mid:])
	return merge(left, right)
}

// 合并func
func merge(left, right []int) []int {
	res := make([]int, len(left)+len(right))
	l, r, i := 0, 0, 0
	for l < len(left) && r < len(right) {
		if left[l] <= right[r] {
			res[i] = left[l]
			l++
		} else {
			res[i] = right[r]
			r++
		}
		i++
	}
	// 左右区,哪个还未遍历完就把对应区的元素加入res
	for l < len(left) {
		res[i] = left[l]
		i++
		l++
	}
	for r < len(right) {
		res[i] = right[r]
		i++
		r++
	}
	/* 加入剩余区的元素的另一种写法
	copy(res[i:], left[l:]) // 如果右区遍历完,执行左区copy
	copy(res[i+len(left)-l:], right[r:]) // 如果左区空,执行这个copy, l=len(left)
	*/
	return res
}

3.堆排序

堆排序(Heap Sort)是利用堆这种数据结构所设计的一种排序算法。

堆的结构是一棵完全二叉树的结构,并且满足堆积的性质:每个节点(叶节点除外)的值都大于等于(或都小于等于)它的子节点。

关于二叉树和完全二叉树的介绍可以参考:二叉树简介

堆排序先按从上到下、从左到右的顺序将待排序列表中的元素构造成一棵完全二叉树,然后对完全二叉树进行调整,使其满足堆积的性质:每个节点(叶节点除外)的值都大于等于(或都小于等于)它的子节点。构建出堆后,将堆顶与堆尾进行交换,然后将堆尾从堆中取出来,取出来的数据就是最大(或最小)的数据。重复构建堆并将堆顶和堆尾进行交换,取出堆尾的数据,直到堆中的数据全部被取出,列表排序完成。

堆的应用

  • 1.堆排序
  • 2.优先级队列
    • 2.1 合并小文件
    • 2.2 高性能定时器
  • 3.取top k元素
  • 4.取中位数
    • 4.1 99%的响应时间

重点
列表排序的算法思路:

  • 1.重复构建堆,并交换堆顶和队尾元素
  • 2.取出队尾数据
  • 3.直到堆中数据全部取出

堆结构分为大顶堆和小顶堆:

    1. 大顶堆:每个节点(叶节点除外)的值都大于等于其子节点的值,根节点的值是所有节点中最大的,所以叫大顶堆,在堆排序算法中用于升序排列。
    1. 小顶堆:每个节点(叶节点除外)的值都小于等于其子节点的值,根节点的值是所有节点中最小的,所以叫小顶堆,在堆排序算法中用于降序排列。

堆排序的原理如下:

    1. 将待排序列表中的数据按从上到下、从左到右的顺序构造成一棵完全二叉树。
    1. 将完全二叉树中每个节点(叶节点除外)的值与其子节点(子节点有一个或两个)中较大的值进行比较,如果节点的值小于子节点的值,则交换他们的位置(大顶堆,小顶堆反之)。
    1. 将节点与子节点进行交换后,要继续比较子节点与孙节点的值,直到不需要交换或子节点是叶节点时停止。比较完所有的非叶节点后,即可构建出堆结构。
    1. 将数据构造成堆结构后,将堆顶与堆尾交换,然后将堆尾从堆中取出来,添加到已排序序列中,完成一轮堆排序,堆中的数据个数减1。
    1. 重复步骤2,3,4,直到堆中的数据全部被取出,列表排序完成。

3.1 大顶堆

代码中,先实现一个bigHeapify(array, start, end)函数,用于比较节点与其子节点中的较大者,如果值小于子节点的值则进行交换。代码中不需要真正将数据都添加到完全二叉树中,而是根据待排序列表中的数据索引来得到节点与子节点的位置关系。完全二叉树中,节点的索引为i,则它的左子节点的索引为2*i+1,右子节点的索引为2*i+2,有n个节点的完全二叉树中,非叶子节点有n//2个,列表的索引从0开始,所以索引为0~n//2-1的数据为非叶子节点。

实现堆排序函数heapSort(array)时,先调用bigHeapify(array, start, end)函数循环对非叶子节点进行调整,构造大顶堆,然后将堆顶和堆尾交换,将堆尾从堆中取出,添加到已排序序列中,完成一轮堆排序。然后循环构建大顶堆,每次将最大的元素取出,直到堆中的数据全部被取出。

堆排序的时间复杂度和稳定性

    1. 时间复杂度
      在堆排序中,构建一次大顶堆可以取出一个元素,完成一轮堆排序,一共需要进行n轮堆排序。每次构建大顶堆时,需要进行的比较和交换次数平均为logn(第一轮构建堆时步骤多,后面重建堆时步骤会少很多)。时间复杂度为 T(n)=nlogn ,再乘每次操作的步骤数(常数,不影响大O记法),所以堆排序的时间复杂度为 O(nlogn) 。
    1. 稳定性
      在堆排序中,会交换节点与子节点,如果有相等的数据,可能会改变相等数据的相对次序。所以堆排序是一种不稳定的排序算法。
      python
      扩展阅读:
  • 堆排、python实现堆排

# 堆排序-大顶堆-升序排列

def maxHeapify(arr: [int], start: int, end: int) -> None:
    """向下调整,堆化"""
    root = start
    # left child index
    child = root*2 + 1
    while child <= end:
        # 比较左右子节点的大值
        if child+1 <= end and arr[child] < arr[child+1]:
            child += 1
        if arr[root] < arr[child]:
            # 子节点更大,交换节点与子节点的值
            arr[root], arr[child] = arr[child], arr[root]
            # 更新节点到子节点位置,继续下一轮交换
            root = child
            child = root*2 + 1
        else:
            break

# 递归法
def maxHeapify_(arr, start, end):
    pos = start
    child = pos*2 + 1
    if child < end:
        right = child + 1
        if right < end and arr[right] > arr[child]:
            child = right
        if arr[child] > arr[pos]:
            arr[pos], arr[child] = arr[child], arr[pos]
            maxHeapify_(arr, pos, end)

def maxheapSort(arr: [int]) -> [int]:
    """大顶堆排序"""
    # 初始化非叶子节点
    first = (len(arr) >> 1) - 1
    for start in range(first, -1, -1):
        # 从下往上,从右至左对每个非叶子节点进行向下调整,循环构成大顶堆
        maxHeapify(arr, start, len(arr)-1)
    # 交换堆顶尾数据,堆数量--,重新堆化
    for end in range(len(arr)-1, 0, -1):
        # 交换堆顶堆尾数据, 构造大顶堆
        arr[0],arr[end] = arr[end], arr[0]
        # 重新调整堆,构造大顶堆
        maxHeapify(arr, 0, end-1)
    return arr


if __name__ == "__main__":
    import random
    nums = list(range(20))
    random.shuffle(nums)
    nums = [19, 1, 11, 4, 12, 8]
    orderNums = maxheapSort(nums)
    print(orderNums)

golang

func maxHeapSort(nums []int) []int {
	// max heap sort
	end := len(nums)-1
	// 从最后一个非叶子节点开始堆化
	for i:=end>>1;i>=0;i-- {
		maxHeapify(nums, i, end)
	}
	// 依次弹出堆顶元素,然后堆化,相当于依次把最大的放尾部
	for i:=end;i>=0;i-- {
		nums[0], nums[i] = nums[i], nums[0]
		end--
		maxHeapify(nums, 0, end)
	}
	return nums
}

func maxHeapify(nums []int, root, end int)  {
	// 大顶堆堆化,堆顶值小就一直下沉
	for {
		// 获取做左孩子
		child := root*2 + 1
		// 越界跳出
		if child > end {
			return
		}
		// 比较左右孩子节点,如果右孩子大则取右孩子与root比较,否则左右孩子左孩子大, child不用++
		if child < end && nums[child] <= nums[child+1] {
			child++
		}
		// 如果父节点值大于左右孩子的大值,说明,已经堆化
		if nums[root] > nums[child] {
			return
		}
		// 交换父节点与较大的子节点
		nums[root], nums[child] = nums[child], nums[root]
		// 更新父节点为孩子节点,继续与孙节点比较,不断往下沉
		root = child
	}
}

3.2 小顶堆

以小顶堆应用的topK问题为例。

TopK
现有n个数,得到前k大的数,显然这需要降序排列,取前k个数即可(k<n)。
使用小顶堆的解决思路:

  • 1.取数组的前k个元素建立一个小顶堆,堆顶就是第k大的元素
  • 2.依次向后遍历数组,对于数组中的元素,如果小于堆顶,忽略,大于堆顶元素,将堆顶替换为该元素,并且调整堆
  • 3.遍历数组的所有元素,倒序弹出堆顶

时间:

  • 1.排序后的切片: O(nlogn)
  • 2.排序的三人组: O(kn)
  • 3.堆排序思路: O(nlogk)

python

# 小顶堆-降序排列

def minHeapify(arr: [int], start: int, end: int):
    """大的节点往下沉,小的往上冒-小顶堆化"""
    root = start
    child = root*2 + 1
    tmpVal = arr[start]
    while child <= end:
        if child + 1 <= end and arr[child+1] < arr[child]:
            child += 1
        if arr[child] < tmpVal:
            arr[root] = arr[child]
            root = child
            child = 2*root + 1
        else:
            arr[root] = tmpVal
            break
    else:
        arr[root] = tmpVal

def minHeapSort(arr: [int], k: int) -> [int]:
    """小顶堆,升序排列, topK"""
    # 以arr切片作为heap的初始序列
    heap = arr[:k]
    # 初始化topK的小顶堆的非叶子节点
    first = (k-2) >> 1
    # 小顶堆,堆化
    for i in range(first, -1, -1):
        minHeapify(heap, i, k-1)
    # 依次遍历arr的第k+1及以后的元素,入小顶堆顶,小顶堆化
    for i in range(k, len(arr)-1):
        if arr[i] > heap[0]:
            heap[0] = arr[i]
            minHeapify(heap, 0, k-1)
    # 排序heap, 小顶堆数量--
    for i in range(k-1, -1, -1):
        heap[0], heap[i] = heap[i], heap[0]
        minHeapify(heap, 0, i-1)
    return heap

def minHeapSort_(arr: [int]) -> [int]:
    """小顶堆,降序输出"""
    length = len(arr)
    # 初始化非叶子结点
    first = (length-2) >> 1
    # 遍历非叶子节点,小堆化
    for i in range(first, -1, -1):
        minHeapify(arr, i, length-1)
    # 排序数组, 小顶堆数量--
    for i in range(length-1, -1, -1):
        arr[0], arr[i] = arr[i], arr[0]
        minHeapify(arr, 0, i-1)
    return arr

if __name__ == "__main__":
    nums = [0, 8, 9, 12, 13, 15, 21, 23]
    # print(minHeapSort(nums, 5)) # top5, 前5的数
    # print(minHeapSort(nums, 8)) # k == len(nums), topK即为原数组的降序排列, copy
    print(minHeapSort_(nums))

golang

func minHeapify(arr []int, root, end int)  {
	// 小顶堆,堆化,非叶子节点大的下沉
	for {
		child := root*2 + 1
		// 越界跳出
		if child > end {
			return
		}
		// root的值和子节点的小值比较
		if child < end && arr[child] >= arr[child+1] {
			child++
		}
		// root节点已经小于子节点,跳出
		if arr[root] < arr[child] {
			return
		}
		// root节点大于等于子节点,交换
		arr[root], arr[child] = arr[child], arr[root]
		root = child
	}
}

func minHeapSort(arr []int) []int  {
	// 小顶堆,降序输出
	length := len(arr)
	// 非叶子节点初始化, int(n/2)
	first := (length-2) >> 1
	// 遍历所有非叶子节点,大的下沉
	for i:=first;i>=0;i-- {
		minHeapify(arr, i, length-1)
	}
	// 依次弹出堆尾节点,交换后继续堆化
	for i:=length-1;i>=0;i-- {
		arr[0], arr[i] = arr[i], arr[0]
		minHeapify(arr, 0, i-1)
	}
	return arr
}

func minHeapSort_(arr []int, k int) []int {
	// 小顶堆, top K
	heap := arr[:k]
	// 初始化非叶子节点
	first := (k-2) >> 1
	// 形成一个数量为k的小顶堆
	for i:=first;i>=0;i-- {
		minHeapify(heap, i, k-1)
	}
	// 遍历后续节点,只有大于堆顶的元素,放入堆顶,堆化
	for i:=k;i<len(arr)-1;i++ {
		if arr[i] > heap[0] {
			heap[0] = arr[i]
			minHeapify(heap, 0, k-1)
		}
	}
	// 依次弹出heap堆中的堆顶元素,弹出的元素就是topK
	for i:=k-1;i>=0;i-- {
		heap[0], heap[i] = heap[i], heap[0]
		minHeapify(heap, 0, i-1)
	}
	return heap
}

补充

4.插入排序 vs 希尔排序

希尔排序可以看做插入排序的升级版。

两种排序算法的时空复杂度:

  • 时间复杂度: 都是O(n^2)
  • 空间复杂度: 都是O(1)

插入排序

python实现

# 插入排序

def insertSort(nums: [int]) -> [int]:
    length = len(nums)
    for i in range(1, length):
        while i > 0 and nums[i-1] > nums[i]:
            nums[i-1], nums[i] = nums[i], nums[i-1]
            i -= 1
    return nums


if __name__ == "__main__":
    import random
    nums = list(range(100))
    random.shuffle(nums)
    randomNums = nums[:10]
    print(randomNums)
    print(insertSort(randomNums))

golang

func insertSort(nums []int) []int {
	length := len(nums)
	for i:=0;i<length;i++ {
		for j:=i;j>0;j-- {
			if nums[j-1] > nums[j] {
				nums[j-1], nums[j] = nums[j], nums[j-1]
			}
		}
	}
	return nums
}

希尔排序

python

# 希尔排序

def shellSort(nums: [int]) -> [int]:
    length = len(nums)
    gap = length >> 1
    while gap:
        for i in range(gap, length):
            while i-gap >= 0 and nums[i-gap] > nums[i]:
                nums[i-gap], nums[i] = nums[i], nums[i-gap]
                i -= gap
        gap >>= 1
    return nums


if __name__ == "__main__":
    import random
    nums = list(range(100))
    random.shuffle(nums)
    randomNums = nums[:10]
    print(randomNums)
    print(shellSort(randomNums))

golang

func shellSort(nums []int) []int {
	length := len(nums)
	gap := length >> 1
	for gap > 0 {
		for i:=gap;i<length;i++ {
			j := i
			for j-gap >= 0 && nums[j-gap] > nums[j] {
				nums[j-gap], nums[j] = nums[j], nums[j-gap]
				j -= gap
			}
		}
		gap >>= 1
	}
	return nums
}
原文地址:https://www.cnblogs.com/davis12/p/15659785.html