排序算法(二)堆排序

一、选择排序的问题

如果有n个数排序,简单排序需要选取一个极值(最大值或者最小值)需要比较n-1次。但是,每一轮比较并没有把以前比较过的结果保存下来,导致下一轮比较的时候会有比较过的数据继续比较大小,这其实影响了效率,做了很多无用功。

堆排序是对简单选择排序的改进。

堆是一种数据结构,是用完全二叉树构建的大顶堆或者小顶堆,有的称为大根堆和小根堆。

二、堆排序的准备知识

1、二叉树

二叉树是至多存在两个子树的树结构。

二叉树的每一个结点度数不超过2(至多2棵子树)

2、深度

根结点为第一层,根的子结点为第二层,第二层的子结点为第三层,以此类推。

树中结点的最大层次称为该树的深度或高度。

3、满二叉树

一棵二叉树的所有分支结点都存在左子树和右子树,并且所有叶子结点只存在在最下面一层。

同样深度二叉树中,满二叉树结点最多。

k为深度(1≤k≤n),则结点总数为2^k-1

如下图,一个深度为4的15个结点的满二叉树:

4、完全二叉树(Complete Binary Tree)

若二叉树的深度为k,二叉树的层数从1到k-1层的结点数都达到了最大个数,在第k层的所有结点都集中在最左边,这就是完全二叉树。

完全二叉树由满二叉树引出。

满二叉树一定是完全二叉树,但完全二叉树不是满二叉树。

k为深度(1≤k≤n),则结点总数最大值为2^k-1,当达到最大值的时候就是满二叉树。

举例,下面几个图都一个完全二叉树,最下一层的叶子结点都连续的集中在左边

下面的图就不是完全二叉树,因为它的结点在最下一层不连续

5、大顶堆和小顶堆

使用完全二叉树来构建的数据结构。

(1)大顶堆

首先上图必须是一个完全二叉树,同时要求每一个非叶子结点的值都要大于等于它的所有子结点的值。

(2)小顶堆

小顶堆也必须是一个完全二叉树,同时要求每一个非叶子结点的值都要小于等于它的所有子结点的值。

三、堆排序(Heap Sort)

有了上面的基础知识,就可以开始堆排序了,本次测试使用大顶堆排序

(一)算法思路

1、构建二叉树

将待排序的数字放在完全二叉树中。由于使用了完全二叉树,比较方便的方法是使用 顺序存储 来描述这个完全二叉树,也就是使用一位数组来表示完全二叉树。

假设待排序的数字为{30,20,80,40,50,10,60,70,90},二叉树表示如下:

为了计算方便,使用下面的数组表示{0,30,20,80,40,50,10,60,70,90}。上图中的序号正好是数组的下标。

2、构建大顶堆

(1)核心算法

选择好起始的节点A,让这个节点与其子结点比较。

假设A有两个子结点B和C,如果子结点中的最大值大于节点A的值,则这个最大值节点和节点A交换位置。

假设A只有一个子结点B,如果B的值大于节点A的值,则交换位置。这种一个子结点的情况是上面的2个子结点的简化。

如果交换后,在新的位置上节点A还是有子结点C和D,那么还是要重复上面的过程,让结点A和结点C、结点D比较。

(2)起点的选择

构建大顶堆,从哪一个节点开始比较呢?

简单来说,从完全二叉树的最下一层的最右边叶子结点的父节点开始。

上图中是从第三层左一结点开始。

上面为了计算方便,数组特殊构造了,由此有以下的关系。

父子结点关系:父结点的下标为i,子结点的下标为2i和2i+1

起始结点和总数n的关系:n就对应最后的叶子结点的下标。因为子结点2i或者(2i+1)计算父结点下标,2i/2或者(2i+1)/2都等于i,所以n/2就是起点的下标。

(3)下一个节点的选择

比较完后,寻找下一个节点继续比较。从当前比较节点向左找兄弟结点,如果这一层找完了,就找上一层最右边结点,直至找到根结点。

构建大顶堆完成后,树结构如下图

3、排序

这是一个迭代的过程。因为使用了大顶堆,每次交换都有很多结点不需要动,这充分体现了,保留了以前排序的结果的思想。

(1)交换大顶堆的根结点和最后一个叶子结点,那么最后一个叶子结点就是最大值,将这个叶子节点排除在排序的节点之外(因为这就是每次找到的最大值)。

(2)从根结点开始(新的根结点),重新调整为大顶堆后,重复上一步。

每一轮从新的根结点调整为大顶堆的过程和上述构建大顶堆的核心算法是一样的,因为用的同一个函数,不过就是起点选择不同,构建大顶堆过程是从n/2开始倒着调整,而堆排序的时候始终从根结点开始。

(二)代码实现(Java)

package heapsort;
import java.util.Arrays;
import java.util.HashSet;
public class HeapSort {
    // 为了计算取下标方便,增加一个0在数组的首位
    // 待比较数个数n为9
    //public static int[] origin = new int[]{0,30,20,80,40,50,10,60,70,90};
    public static int[] origin = new int[]{0,50,10,90,30,70,40,80,60,20};
    // indexSet数组记录每一次变化的结点的索引,仅是为了方便观察
    private static HashSet<Integer> indexSet = new HashSet<Integer>();
    public static void main(String[] args) {
        int total = origin.length-1;
        System.out.println("Unsorted: "+Arrays.toString(origin));
        // 构建大顶堆
        for (int i = total/2,k=1; i >= 1; i--,k++) {
            heapAdjust(total,i,origin);
            // 打印每一次调整的结果及交换的下标
            System.out.println(k + "\t" + Arrays.toString(origin) + " " + Arrays.toString(indexSet.toArray()));
        }
        System.out.println();
        // 堆排序
        for (;total>1;) {
            System.out.println(total);
            // 交换根结点和最后一个叶子结点
            swap(1, total, origin);
            System.out.println("Swapped: " + Arrays.toString(origin));
            // 总数减1,因为最后一个叶子结点现在是最大值,要排除在排序数字之外
            total--;
            if (total==2 && origin[total]>=origin[total-1]) {
                // 改进的地方:如果只剩2个结点比较,且第二个结点大于等于根结点,没有必要再调整了,可以直接退出了
                break;
            }
            heapAdjust(total,1,origin);
            // 打印每一次调整的结果及交换的下标
            System.out.println("Changed: " + Arrays.toString(origin) + " " + Arrays.toString(indexSet.toArray()));
            System.out.println();
        }
        System.out.println("Sorted: "+Arrays.toString(origin));
    }
    /**
     * adjustHeap 调整为大顶堆结构
     * @param n 待比较数的个数
     * @param index 每次比较的结点下标
     * @param array 待比较的数组
     */
    public static void heapAdjust(int n,int index,int [] array){
        indexSet.clear();
        int maxChildrenIndex;
        for (int i = 2*index; i <= n; i = index<<1) {
            maxChildrenIndex = i;
            if (i<n && array[i]<array[i+1]) {
                // 小于n说明还有右边的结点,选出最大子结点的下标
                maxChildrenIndex=i+1;
            }
            // 等于n,说明只有一个子结点;小于n,同时左边结点大于右边结点,这两种情况maxindex都不用改变了。
            if (array[maxChildrenIndex]>array[index]) {
                swap(index, maxChildrenIndex, array);
                // 记录交换的结点index,便于观察
                indexSet.add(index);
                indexSet.add(maxChildrenIndex);
                // 调整后,需要重设比较的起始结点,看这个结点和它的子结点是否还要需要调整
                index = maxChildrenIndex;
            }
            else {
                break;
            }
        }
    }
    public static void swap(int index1,int index2,int [] array){
        try {
            int temp = origin[index1];
            origin[index1] = origin[index2];
            origin[index2] = temp;
        } catch (Exception e) {
            throw new ArrayIndexOutOfBoundsException();
        }
    }
}

(三)总结

1、时间复杂度

假设结点数为n,完全二叉树的深度为k

在构建堆的过程中,因为我们是完全二叉树从最下层最右边的非终端结点开始构建,将它与其孩子进行比较和若有必要的互换,对于每个非终端结点来说,其实最多进行两次比较和互换操作,因此整个构建堆的时间复杂度为O(n)。

在正式排序时,第i次取堆顶记录重建堆需要用O(logn)的时间,并且需要循环取n-1次堆顶记录,因此,重建堆的时间复杂度为O(nlogn)。

所以总体来说,堆排序的时间复杂度为O(nlogn)。由于堆排序对原始记录的排序状态并不敏感,因此它无论是最好、最坏和平均时间复杂度均为O(nlogn)。

2、空间复杂度

就使用了一个交换用的空间,空间复杂度为O(1)

3、稳定性

不稳定的排序方法。

参考

http://baike.baidu.com/view/157305.htm

《大话数据结构》

时间: 2024-10-18 18:33:07

排序算法(二)堆排序的相关文章

排序算法系列——堆排序

记录学习点滴,菜鸟成长记 堆排序引入了另一种算法设计技巧:使用一种我们称之为“堆”的数据结构来进行数据管理. 堆排序算是真正意义上的利用数据结构来求解数组排序的方法. “插入排序”和“归并排序”可以看做是一种“计算机体力活”,体现的思想更多的是去模拟最简单的人类思维,比如插入排序过程中的比较,归并中子问题合并时的比较. “堆排序”可以看做是“计算机脑力活”,他利用了一种结构化的语言来表达,这种结构化带来一些性质,比如左右孩子.比[堆大小的一半向下取整]大的下标都是叶节点不需要维护其最大堆性质等.

排序算法之堆排序(Heapsort)解析

一.堆排序的优缺点(pros and cons) (还是简单的说说这个,毕竟没有必要浪费时间去理解一个糟糕的的算法) 优点: 堆排序的效率与快排.归并相同,都达到了基于比较的排序算法效率的峰值(时间复杂度为O(nlogn)) 除了高效之外,最大的亮点就是只需要O(1)的辅助空间了,既最高效率又最节省空间,只此一家了 堆排序效率相对稳定,不像快排在最坏情况下时间复杂度会变成O(n^2)),所以无论待排序序列是否有序,堆排序的效率都是O(nlogn)不变(注意这里的稳定特指平均时间复杂度=最坏时间复

排序算法: 堆排序法

一,使用堆排序法之前,需要了解堆的特性: 1,堆一般都用数组的方式来存储,堆分为“最大堆”和“最小堆”: 2,以“最大堆”为例: (1)一个节点最多有两个子节点,即左右节点,每个节点都是一个堆: (2)父节点的值不小于子节点的值: (3)一个i节点,其父节点为(i-1)/2,左节点(2*i+1),右节点(2*i+2) 一个最大堆例子: 数组 int a[]: 83 78 81 48 17 27 二,将一个数组形成堆(以最大堆为例) 数组a[]: 27 48 81 78 17 83 形成最大堆思路

基本排序算法&lt;二&gt;

希尔排序 原理:希尔排序也称之为递减增量排序,它是对插入排序的改进.在插入排序中,我们知道,插入排序对于近似已排好序的序列来说,效率很高,可以达到线性排序的效率.但是插入排序效率也是比较低的,他一次只能将数据向前移一位.比如如果一个长度为N的序列,最小的元素如果恰巧在末尾,那么使用插入排序仍需一步一步的向前移动和比较,要N-1次比较和交换.希尔排序通过将待比较的元素划分为几个区域来提升插入排序的效率.这样可以让元素可以一次性的朝最终位置迈进一大步,然后算法再取越来越小的步长进行排序,最后一步就是

排序算法(二)之希尔排序

希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法.希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一.本文会以图解的方式详细介绍希尔排序的基本思想及其代码实现. 基本思想 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序:随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止. 简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然

图解排序算法(二)之希尔排序

希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法.希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一.本文会以图解的方式详细介绍希尔排序的基本思想及其代码实现. 基本思想 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序:随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止. 简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然

常用排序算法二

SELECTION SORT:选择排序算法,每次从未完成排序的部分选出最小的插入未完成排序元素的最前面 代码实现比较好写: import java.util.*; public class SelectSort {     public static void main(String[] args)      {         System.out.println("Hello World!"); int [] a = {3,44,38,5,47,15,36,26,27,2,46,4

[排序算法二]选择排序

选择排序(Selection sort)是一种简单直观的排序算法.它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾.以此类推,直到全部待排序的数据元素的个数为零.选择排序是不稳定的排序方法. 算法性能 时间复杂度:O(n^2),总循环次数 n(n-1)/2.数据交换次数 O(n),这点上来说比冒泡排序要好,因为冒泡是把数据一位一位的移上来,而选择排序只需要在子循环结束后移动一次

排序算法(二)选择排序---堆排序

概念:利用树结构进行排序. 分类:1.大顶堆: 每个小树的根节点都大于子节点   升序排序使用大顶堆 2.小顶堆:每个小树的子节点都大于根节点 降序排序使用小顶堆 1 public class HeapSort { 2 3 public static void main(String[] args){ 4 int[] arr=new int[]{9,6,7,0,1,10,4,2}; 5 System.out.println(Arrays.toString(arr)); 6 heapSort(ar

排序算法之堆排序(优先队列)

1.堆排序的堆,其实是一个 完全二叉树.既是一个结点要么是叶子结点,要么必定有左右两个子节点的树. 2.堆有序:每个结点的值,都必须大于两个子节点.但是两个子结点的大小不作要求. 3.一棵大小为N的完全二叉树,高度为lgN(层). 用数组实现堆,假设数组下标从0开始,下标为k的元素,它的左子树是2k+1,右子树是左子树+1,即2k+2 一:由上至下的有序化(下沉) 如果堆的有序状态,因为某个结点比它的两个子结点或者其中之一小而打破了,那么可以通过与两个子结点中的较大者来交换. 交换后可能会在子结