堆排序——Java与Go实现

本文详细介绍了堆排序的概念,包括完全二叉树的构建,大顶堆和小顶堆的理解,以及如何通过调整堆来实现排序。通过Java和Go两种语言分别展示了升序和降序的堆排序代码实现,最后分析了堆排序的效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概念分析

堆排序是使用堆这种数据结构进行排序的方法。(好像是废话)

思路分析
  1. 首先,我们将待排序的数组看作一个完全二叉树
  2. 将此二叉树转成大顶堆或者小顶堆
  3. 将堆顶元素与堆的最后一个元素互换,之后丢弃最后一个元素
  4. 重复第二步与第三步,直到堆只剩一个堆顶
具体解析
  1. 什么是完全二叉树?
    对于一个深度为k,有n个节点的二叉树,其所有的结点与深度为k的满二叉树对应的编号一样,则称之为完全二叉树。
  2. 数组如何转成完全二叉树?
    举个栗子,有一个数组arr = {1,2,3,4,5},那我们可以将其构建为以下的二叉树
    在这里插入图片描述
    那么我们从这个图可以看出,数组里面下标0对应根结点,之后所有结点的左子结点对应的下标为 2 * n + 1,n就是该分支节点的下标索引,右子结点对应的就是 2 * n + 2;以此规则,虽然该数组依然是个数组,但是我们从逻辑上将其构建成了完全二叉树。
  3. 大顶堆和小顶堆是什么?
    大顶堆和小顶堆都是类似完全二叉树,或者其就是一个完全二叉树,不过其具有一个特点:父结点必然大于或者小于其左子结点或者其右子结点。以完全二叉树分析来看,就是其根节点是最大或者最小的元素。
  4. 如何将完全二叉树构建成大顶堆或者小顶堆
    1) 我们首先从最后一个分支节点开始,如上述的例子,最后一个分支节点是2,以此类推,如果数组为{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13},那么其最后一个分支节点就是6(因为13对应的下标12,,1 2 = 2 * 5 + 2, 11 = 2 * 5 + 1,最后两个结点的父结点自然是最后一个分支结点 )
    2) 我们将最后一个分支结点与其左子结点和其右子结点比较,选出最小或者最大的作为分支结点的值
    1. 之后我们选择该分支结点的前一个分支结点再进行排序,知道我们排到根节点,此时所有的分支结点的值都小于或者大于其左右子结点,此时,该完全二叉树构建成为了堆。
  5. 如何进行排序?
    我们将堆顶元素与最后一个元素换位置,如上,我们将1与5换位置,之后将1丢弃。此时堆不满足最小堆,我们再将其构建为小顶堆,再进行换位置,此时被丢弃的元素其实是在数组的最后一个,最后小顶堆排序下来的就是一个降序的有序数组。
    在这里插入图片描述
    在这里插入图片描述
    此时数组变成:
    在这里插入图片描述
    当我们重复上述步骤的时候,最后的结果就会是:5 4 3 2 1
代码实现

思路分析一大堆,还是直接上代码吧
首先java进行堆排序的升序排序:

package tree;

import java.util.Arrays;
import java.util.Random;

public class HeapSort {
    public static void main(String[] args) {
        // 建立一个无序的顺序存储二叉树
        int[] arr = new int[80000];
        Random random = new Random();
        for (int i = 0; i < arr.length; i++){
            arr[i] = random.nextInt(800000);
        }
        System.out.println(System.currentTimeMillis());
        heapSort(arr);
        System.out.println(System.currentTimeMillis());
        System.out.println(Arrays.toString(arr));

    }
	// 该方法用于进行排序
    public static  void heapSort(int[] arr){
        // 1. 第一步,将顺序存储二叉树转化成大顶堆 9 6 8 5 4
        // 2. 第二步,将堆顶元素与最后一个元素交换
        // 3. 第三步,将顺序存储二叉树转化成大顶堆
        for (int i = arr.length / 2 - 1; i >= 0; i--){
            adjustHeap(arr, i, arr.length);
        }
        for (int j = arr.length - 1; j>= 0; j --){
        	// 换位置
            int temp = arr[j];
            arr[j] = arr[0];
            arr[0] = temp;
            // 每次j会自减 那么就相当于把数组的最后一个元素丢弃了
            adjustHeap(arr, 0, j);
        }
    }

    /**
     *
     * @param arr 待排序的数组
     * @param i 最下面的分支节点
     * @param length 需要调整的长度
     */
    public static void adjustHeap(int[] arr, int i, int length){
        // 记录一下最开始进入循环之前的分支节点的值
        int temp = arr[i];
        // 取左子孩子节点
        for (int k = i * 2 + 1; k < length; k = 2 * k + 1){
            // 如果当前分支节点有右孩子,并且右孩子比左孩子大,那么我们去比较右孩子
            if (k + 1 < length && arr[k + 1] > arr[k]){
                k++;
            }
            // 如果孩子节点比该父节点大,父节点换成孩子节点
            if (arr[k] > temp){
                arr[i] = arr[k];
                i = k; // 相当于递归,再去查该孩子节点的孩子节点
            }else {
                break;
            }
        }
        arr[i] = temp;
    }

}


之后是Go实现的小顶堆的降序排序:

package main

import (
	"fmt"
	"time"
	"math/rand"
)
var count int
func main(){
	arr := make([]int, 80000)
	t := time.Now()
	fomat := t.UnixNano()
	rand.Seed(fomat)
	for i := 0; i < len(arr); i++{
		arr[i] = rand.Intn(80000)
	}
	t1 := time.Now()
	heapSort(arr)
	t2 := time.Since(t1)
	fmt.Println(t2)
	// fmt.Println(arr)
	// arr := make([]int, 8)
	// queen8(arr, 0)
	// fmt.Println(count)
}
// 堆排序
// 1. 先将完全二叉树转化成大顶堆或者小顶堆
// 2. 将堆的根节点和最后一个叶子节点进行交换
// 3. 将树最后一个节点去掉,继续执行1,2,3

func heapSort(arr []int){
	// 这个循环是什么意思呢?
	// 我们有了调整小顶堆的方法,但是有个问题,我们只能调节下标为i的子树
	// 那我们就拿到最后一个分支节点,将其变为小顶堆
	// 之后将下标减一,该下标对应的也是一个分支节点,继续调整该分支节点的子树
	for i := len(arr) / 2 - 1; i >= 0; i--{
		adjust(arr, i, len(arr))
	}
	// 现在原本的二叉树已经是一个小顶堆
	for j := len(arr) - 1; j > 0; j--{
		// 换位置 没什么好说的
		temp := arr[j]
		arr[j] = arr[0]
		arr[0] = temp
		// 调整根结点为小顶堆
		// 为什么不重新从最后一个分支节点开始呢?
		// 因为此时根结点和最后一个元素换位置,那么根结点肯定大于他的左右子节点,我们将根结点对应的树调整为小顶堆即可
		// 每次传入的长度要减少,因为我们逻辑上来说是将最后一个分支节点去除
		adjust(arr, 0, j)
	}
}

// 将二叉树转成小顶堆
func adjust(arr []int, i, length int){
		// 先用一个变量记录i位置的值
		// i位置的值代表什么呢? 代表当前分支节点在数组中的坐标
		// 我们要拿当前分支节点作为根结点,将分支节点看作一个二叉树
		// 将这个小树苗转成小顶堆,所以需要比较左右子节点与分支节点的值
		temp := arr[i]
	for k := 2 * i + 1; k < length; k = k * 2 + 1{
		// 如果右子结点比左子结点小
		if (k + 1 < length && arr[k + 1] < arr[k]){
			k++
		}
		if (arr[k] < temp){
			arr[i] = arr[k]
			i = k
		}else{
			break
		}
	}
	// 此时这个子树里面是含有两个重复的数的 因为我们一直在使用arr[i] = arr[k] 
	// 所以此时我们将最后的i的对应的值赋值为temp
	arr[i] = temp
}
效率分析

Java:
在这里插入图片描述
Go:
在这里插入图片描述
可以看到 堆排序在效率上还是狠给力的。


just do it!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值