1. 不得不说说二叉树
要了解堆首先得了解一下二叉树,在计算机科学中,二叉树是每个节点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。
二叉树的每个结点至多只有二棵子树(不存在度大于 2 的结点),二叉树的子树有左右之分,次序不能颠倒。二叉树的第 i 层至多有 2i - 1 个结点;深度为 k 的二叉树至多有 2k - 1 个结点;对任何一棵二叉树 T,如果其终端结点数为 n0,度为 2 的结点数为 n2,则n0 = n2 + 1。
树和二叉树的三个主要差别:
树的结点个数至少为 1,而二叉树的结点个数可以为 0
树中结点的最大度数没有限制,而二叉树结点的最大度数为 2
树的结点无左、右之分,而二叉树的结点有左、右之分
二叉树又分为完全二叉树(complete binary tree)和满二叉树(full binary tree)
满二叉树:一棵深度为 k,且有 2k - 1 个节点称之为满二叉树
(深度为 3 的满二叉树 full binary tree)
完全二叉树:深度为 k,有 n 个节点的二叉树,当且仅当其每一个节点都与深度为 k 的满二叉树中序号为 1 至 n 的节点对应时,称之为完全二叉树
(深度为 3 的完全二叉树 complete binary tree)
2. 什么是堆?
堆(二叉堆)可以视为一棵完全的二叉树,完全二叉树的一个“优秀”的性质是,除了最底层之外,每一层都是满的,这使得堆可以利用数组来表示(普通的一般的二叉树通常用链表作为基本容器表示),每一个结点对应数组中的一个元素。
如下图,是一个堆和数组的相互关系
(堆和数组的相互关系)
对于给定的某个结点的下标 i,可以很容易的计算出这个结点的父结点、孩子结点的下标:
Parent(i) = floor(i/2),i 的父节点下标
Left(i) = 2i,i 的左子节点下标
Right(i) = 2i + 1,i 的右子节点下标
二叉堆一般分为两种:最大堆和最小堆。
最大堆:
最大堆中的最大元素值出现在根结点(堆顶)
堆中每个父节点的元素值都大于等于其孩子结点(如果存在)
(最大堆)
最小堆:
最小堆中的最小元素值出现在根结点(堆顶)
堆中每个父节点的元素值都小于等于其孩子结点(如果存在)
(最小堆)
3. 堆排序原理
堆排序就是把最大堆堆顶的最大数取出,将剩余的堆继续调整为最大堆,再次将堆顶的最大数取出,这个过程持续到剩余数只有一个时结束。在堆中定义以下几种操作:
最大堆调整(Max-Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
创建最大堆(Build-Max-Heap):将堆所有数据重新排序,使其成为最大堆
堆排序(Heap-Sort):移除位在第一个数据的根节点,并做最大堆调整的递归运算
继续进行下面的讨论前,需要注意的一个问题是:数组都是 Zero-Based,这就意味着我们的堆数据结构模型要发生改变
(Zero-Based)
相应的,几个计算公式也要作出相应调整:
Parent(i) = floor((i-1)/2),i 的父节点下标
Left(i) = 2i + 1,i 的左子节点下标
Right(i) = 2(i + 1),i 的右子节点下标
最大堆调整(MAX‐HEAPIFY)的作用是保持最大堆的性质,是创建最大堆的核心子程序,作用过程如图所示:
(Max-Heapify)
Memandangkan selepas satu pelarasan, timbunan masih melanggar sifat timbunan, ujian rekursif diperlukan untuk menjadikan keseluruhan timbunan memenuhi sifat timbunan Ini boleh dinyatakan dalam JavaScript seperti berikut:
/** * 从 index 开始检查并保持最大堆性质 * * @array * * @index 检查的起始下标 * * @heapSize 堆大小 * **/ function maxHeapify(array, index, heapSize) { var iMax = index, iLeft = 2 * index + 1, iRight = 2 * (index + 1); if (iLeft < heapSize && array[index] < array[iLeft]) { iMax = iLeft; } if (iRight < heapSize && array[iMax] < array[iRight]) { iMax = iRight; } if (iMax != index) { swap(array, iMax, index); maxHeapify(array, iMax, heapSize); // 递归调整 } } function swap(array, i, j) { var temp = array[i]; array[i] = array[j]; array[j] = temp; }
Secara umumnya, rekursi digunakan terutamanya dalam kaedah bahagi dan takluk, tetapi bahagi dan takluk tidak diperlukan di sini. Selain itu, panggilan rekursif memerlukan menolak/mengosongkan timbunan, yang mempunyai sedikit kelemahan prestasi berbanding dengan lelaran. Sudah tentu, mengikut peraturan 20/80, ini boleh diabaikan. Tetapi jika anda merasakan bahawa menggunakan rekursi akan membuat anda berasa tidak selesa, anda juga boleh menggunakan lelaran, seperti berikut:
/** * 从 index 开始检查并保持最大堆性质 * * @array * * @index 检查的起始下标 * * @heapSize 堆大小 * **/ function maxHeapify(array, index, heapSize) { var iMax, iLeft, iRight; while (true) { iMax = index; iLeft = 2 * index + 1; iRight = 2 * (index + 1); if (iLeft < heapSize && array[index] < array[iLeft]) { iMax = iLeft; } if (iRight < heapSize && array[iMax] < array[iRight]) { iMax = iRight; } if (iMax != index) { swap(array, iMax, index); index = iMax; } else { break; } } } function swap(array, i, j) { var temp = array[i]; array[i] = array[j]; array[j] = temp; }
Fungsi mencipta timbunan maksimum (Build-Max-Heap) adalah untuk mengubah tatasusunan menjadi timbunan maksimum. Ia menerima dua parameter: susunan dan saiz timbunan akan memanggil Max-Heapify atas. Ubah tatasusunan dan buat timbunan maksimum. Oleh kerana Max-Heapify boleh memastikan bahawa nod selepas nod dengan subskrip i memenuhi sifat timbunan maksimum, panggilan bawah ke atas Max-Heapify boleh mengekalkan sifat ini semasa proses transformasi. Jika bilangan elemen dalam timbunan maksimum ialah n, maka Build-Max-Heap bermula dari Parent(n) dan memanggil Max-Heapify ke atas. Prosesnya adalah seperti berikut:
Diterangkan dalam JavaScript seperti berikut:
function buildMaxHeap(array, heapSize) { var i, iParent = Math.floor((heapSize - 1) / 2); for (i = iParent; i >= 0; i--) { maxHeapify(array, i, heapSize); } }
Isih Timbunan ialah algoritma antara muka isihan Timbunan terlebih dahulu memanggil Build-Max-Heap untuk mengubah tatasusunan menjadi timbunan maksimum, kemudian menukar elemen atas dan bawah timbunan, kemudian menaikkan bahagian bawah, dan akhirnya Recall Max-Heapify untuk mengekalkan sifat timbunan maksimum. Memandangkan elemen atas timbunan mestilah elemen terbesar dalam timbunan, selepas satu operasi, unsur terbesar yang wujud dalam timbunan dipisahkan daripada timbunan Selepas mengulangi n-1 kali, tatasusunan disusun. Seluruh proses adalah seperti berikut:
Diterangkan dalam JavaScript seperti berikut:
function heapSort(array, heapSize) { buildMaxHeap(array, heapSize); for (int i = heapSize - 1; i > 0; i--) { swap(array, 0, i); maxHeapify(array, 0, i); } }
4.Pelaksanaan bahasa JavaScript
Akhir sekali, susun perkara di atas ke dalam kod javascript lengkap seperti berikut:
function heapSort(array) { function swap(array, i, j) { var temp = array[i]; array[i] = array[j]; array[j] = temp; } function maxHeapify(array, index, heapSize) { var iMax, iLeft, iRight; while (true) { iMax = index; iLeft = 2 * index + 1; iRight = 2 * (index + 1); if (iLeft < heapSize && array[index] < array[iLeft]) { iMax = iLeft; } if (iRight < heapSize && array[iMax] < array[iRight]) { iMax = iRight; } if (iMax != index) { swap(array, iMax, index); index = iMax; } else { break; } } } function buildMaxHeap(array) { var i, iParent = Math.floor(array.length / 2) - 1; for (i = iParent; i >= 0; i--) { maxHeapify(array, i, array.length); } } function sort(array) { buildMaxHeap(array); for (var i = array.length - 1; i > 0; i--) { swap(array, 0, i); maxHeapify(array, 0, i); } return array; } return sort(array); }
5. Aplikasi algoritma isihan timbunan
(1) Prestasi/kerumitan algoritma
Kerumitan masa bagi jenis timbunan adalah sangat stabil (kita dapat melihat bahawa ia tidak sensitif kepada data input), dan kerumitan O(n㏒n) Kes terbaik adalah sama dengan kes terburuk.
Walau bagaimanapun, kerumitan ruangnya berbeza dari pelaksanaan ke pelaksanaan. Dua kerumitan biasa telah dibincangkan di atas: O(n) dan O(1). Selaras dengan prinsip penjimatan ruang, saya mengesyorkan kaedah kerumitan O(1).
(2) Kestabilan algoritma
Isihan timbunan melibatkan sebilangan besar proses penyaringan dan pergerakan serta merupakan algoritma pengisihan yang tidak stabil.
(3) Algoritma senario terpakai
Isih timbunan akan menyebabkan overhed yang agak besar dalam proses penubuhan dan pelarasan timbunan, dan tidak sesuai apabila terdapat sedikit elemen. Walau bagaimanapun, ia masih merupakan pilihan yang baik apabila terdapat banyak elemen. Terutama apabila menyelesaikan masalah seperti "nombor pertama n terbesar", ia hampir menjadi algoritma pilihan.