zoukankan      html  css  js  c++  java
  • 几个常用排序的代码实现堆排序|快排|归并排序 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
    }
    
  • 相关阅读:
    耐人寻味的 8 幅Java技术图
    什么是线程安全?怎么样才能做到线程安全?
    线程池
    ExecutorService的正确关闭方法
    js中let和var定义变量的区别
    sql的left join 、right join 、inner join之间的区别
    Collections.sort排序
    Mysql声明变量及使用
    java集合容器汇总
    TortoiseSVN提交commit提示Unable to create pristine install stream.系统找不到指定的路径 之解决方法
  • 原文地址:https://www.cnblogs.com/davis12/p/15659785.html
Copyright © 2011-2022 走看看