二叉搜索树的删除操作可以交换吗_堆和二叉堆的实现和特性

2514c163095f989ac5f215215f609269.png

作者公众号:一角钱技术(org_yijiaoqian)

Heap

Heap:可以迅速找到一堆数中的最大或者最小值的数据结构。

将根节点最大的堆叫做大顶堆或大根堆,根节点最小的堆叫做小顶堆或小根堆

常见的堆有二叉堆裴波那契堆等。

堆本身是一个相对比较抽象的数据结构,那么它有具体的实现就分为二叉堆(二项堆、Binary)、裴波那契堆(基于树的)。那么要实现的话一般面试来说或者经常会用的话就是二叉堆来实现,当然在工业级比较牛逼的应用都是裴波那契以及非常严格裴波那契。裴波那契堆因为相对比较复杂,你可以不知道去怎么实现,但是你可以看它的时空复杂度更好,它的实现也是基于树,但是并不少二叉树而是多叉的。

假设是大顶堆,则常见操作(API):

  • find-max:O(1)
  • delete-max:O(logN)
  • insert(create):O(logN) or O(1)

“堆” 这种数据结构为什么会有用? 在很多线上中的情形经常会在使用,比如说经常一个数一个数的过来,同时还有从另外一边的化经常去删除一些数据,问你这些数据里面它最大值是多少?这里的最大值可能就代表优先级最高的结点或者是那个数需要你先处理的,比如说你的任务流里面随时你要拿出优先级最高的任务优先处理,那么这种数据结构就更加的好用。

有些人可能会想我用数组来实现可不可以?或者是我直接排序可不可以?这里我们可以思考一下: 假设维护一个数组,每次有个新元素插入进来,你就需要把整个数组进行一次所谓的排序,这样的话时间复杂度就会是 nlogn,这样的话就是不够高效的,另外删除也可能比较麻烦,假设删除的是最大值或最小值在最后面的话还好,可以是 O(1) 的时间复杂度,但是如果在最前面的话,你就必须把整个数组最前面元素删除掉之后,把整个数组往前挪,这样时间复杂度又变差了。

不同的实现比较:https://en.wikipedia.org/wiki/Heap_(data_structure)

b4952c4fc7ae4debf6e0ba708457ba11.png

当你提到“堆” 的时候,不要默认认为是二叉堆,同时你要知道堆的实现又很多种,而二叉堆本身的话只是因为它相对比较容易实现,它的时间效率是堆里面算比较差的。

二叉堆的性质

通过完全二叉树来实现(注意:不是二叉搜索树);

什么是完全二叉树? 就是它的根和每一级结点都是满的,除了最下面一层叶子结点可能不满之外,其他上面结点都是满的。

二叉堆(大顶)它满足下列性质:

  • 性质1:是一颗完全树;
  • 性质2:树中任意结点的值总数 >= 其子节点的值;
c117f6196459ec5b534a3409eeaed815.png

由于性质2,就可以保证根结点是最大的结点,所以我们访问最大值就直接返回这个根结点值。这里思考一个问题:为什么要做成树的结构呢?其实就要方便,因为找最大值是O(1)了是满足的,但是问题是后续你要删除一个元素或者你要添加一个元素,怎么让这个操作高效,至少是 logn 的。

二叉堆的实现细节

  1. 二叉堆一般都通过 “数组” 来实现
  2. 假设“第一个元素” 在数组中的索引为 0 的话,则父结点和子结点的位置关系如下:索引为 i 的左孩子的索引是 (2∗i+1);索引为 i 的右孩子的索引是 (2∗i+2);索引为 i 的父结点的索引是 floor((i−1)/2);

因为把一个完全二叉树依次放到一维数组里面去,那么它的孩子和父亲之间的关系,就可以直接用下班的数学关系表示了。

6b27f7afb2dbffe7265fca085ec4c617.png

Insert 插入操作

  1. 新元素一律先插入到堆的尾部
  2. 依次向上调整整个堆的结构(一直到根即可) HeapifyUp

当这个堆要进行维护操作,也就是插入元素的时候以及你要删除元素的时候要怎么做?

例子:85 添加到二叉堆中

9139a5a84467d63260d241c264024c1a.png

操作的细节分为两步:

  • 第一步:首先把新元素插入到堆的尾部再说;(新的元素可能是特别大或者特别小,那么要做的一件事情就是重新维护一下堆的所有元素,把新元素挪到这个堆的相应的位置)
  • 第二步:依次向上调整整个堆的结构,就叫 HeapifyUp
949ea7a4b00fffff075edeff10ca3d52.png

85 按照上面讲的先插入到堆的尾部,也就是一维数组的尾部,一维数组的尾部的话就上图的位置,因为这是一个完全二叉树,所以它的尾部就是50后面这个结点。插进来之后这个时候就破坏了堆,它的每一个结点都要大于它的儿子的这种属性了,接下来要做的事情就是要把 85 依次地向上浮动,怎么浮动?就是 85 大于它的父亲结点,那么就和父亲结点进行交换,直到走到根如果大于根的话,就和根也进行交换。

334f5be884f32dddaa9a0065b5519e7f.png

85 再继续往前走之后,它要和 80 再进行比较,同理可得:也就是说这个结点每次和它的父亲比,如果它大于它的父亲的话就交换,直到它不再大于它的父亲。

3a8cefb0fe5477b43d7de0c170d8a7d3.png

Delete Max 删除堆顶操作

  1. 将堆尾元素替换到顶部(即堆顶被替代删除掉)
  2. 依次从根部向下调整整个堆的结构(一直到堆尾即可) HeapifyDown

例子:90 从二叉堆中删除

e05a7247d37ea80beecf81d75936fa4c.png

堆的实现代码模版

Java 版本

// Javaimport java.util.Arrays;import java.util.NoSuchElementException;public class BinaryHeap {    private static final int d = 2;    private int[] heap;    private int heapSize;    /**     * This will initialize our heap with default size.     */    public BinaryHeap(int capacity) {        heapSize = 0;        heap = new int[capacity + 1];        Arrays.fill(heap, -1);    }    public boolean isEmpty() {        return heapSize == 0;    }    public boolean isFull() {        return heapSize == heap.length;    }    private int parent(int i) {        return (i - 1) / d;    }    private int kthChild(int i, int k) {        return d * i + k;    }    /**     * Inserts new element in to heap     * Complexity: O(log N)     * As worst case scenario, we need to traverse till the root     */    public void insert(int x) {        if (isFull()) {            throw new NoSuchElementException("Heap is full, No space to insert new element");        }        heap[heapSize] = x;        heapSize ++;        heapifyUp(heapSize - 1);    }    /**     * Deletes element at index x     * Complexity: O(log N)     */    public int delete(int x) {        if (isEmpty()) {            throw new NoSuchElementException("Heap is empty, No element to delete");        }        int maxElement = heap[x];        heap[x] = heap[heapSize - 1];        heapSize--;        heapifyDown(x);        return maxElement;    }    /**     * Maintains the heap property while inserting an element.     */    private void heapifyUp(int i) {        int insertValue = heap[i];        while (i > 0 && insertValue > heap[parent(i)]) {            heap[i] = heap[parent(i)];            i = parent(i);        }        heap[i] = insertValue;    }    /**     * Maintains the heap property while deleting an element.     */    private void heapifyDown(int i) {        int child;        int temp = heap[i];        while (kthChild(i, 1) = heap[child]) {                break;            }            heap[i] = heap[child];            i = child;        }        heap[i] = temp;    }    private int maxChild(int i) {        int leftChild = kthChild(i, 1);        int rightChild = kthChild(i, 2);        return heap[leftChild] > heap[rightChild] ? leftChild : rightChild;    }    /**     * Prints all elements of the heap     */    public void printHeap() {        System.out.print("nHeap = ");        for (int i = 0; i 

C/C++ 版本

C/C++#include using namespace std;class BinaryHeap {public:    BinaryHeap(int capacity);    void insert(int x);    int erase(int x);    int findMax();    void printHeap();    bool isEmpty() { return heapSize == 0; }    bool isFull() { return heapSize == capacity; }    ~BinaryHeap() { delete[] heap; }private:    void heapifyUp(int i);    void heapifyDown(int i);    int maxChild(int i);    int parent(int i) { return (i - 1) / 2; }    int kthChild(int i, int k) { return 2 * i + k; }private:    int *heap;    int heapSize;    int capacity;};/** * This will initialize our heap with default size.*/BinaryHeap::BinaryHeap(int capacity) {    this->heapSize = 0;    this->capacity = capacity;    this->heap = new int[capacity + 5];}/** * Inserts new element in to heap * Complexity: O(log N) * As worst case scenario, we need to traverse till the root */void BinaryHeap::insert(int x) {    try {        if (isFull())             throw -1;                heap[heapSize] = x;        heapSize ++;        heapifyUp(heapSize - 1);        return ;    } catch (int e) {        cout < 0 && insertValue > heap[parent(i)]) {        heap[i] = heap[parent(i)];        i = parent(i);    }    heap[i] = insertValue;}/** * Maintains the heap property while deleting an element. */void BinaryHeap::heapifyDown(int i) {    int child;    int temp = heap[i];    while (kthChild(i, 1) = heap[child]) {            break;        }        heap[i] = heap[child];        i = child;    }    heap[i] = temp;}int BinaryHeap::maxChild(int i) {    int leftChild = kthChild(i, 1);    int rightChild = kthChild(i, 2);    return heap[leftChild] > heap[rightChild] ? leftChild : rightChild;}/** * This method returns the max element of the heap. * complexity: O(1) */int BinaryHeap::findMax() {    try {        if (isEmpty())             throw -1;        return heap[0];    } catch (int e) {        cout <

Javascript 版本

// JavaScriptclass BinaryHeap {  constructor(compare) {    this.data = [];    this.compare = compare;  }  insert(value) {    this.insertAt(this.data.length, value);  }  insertAt(index, value) {    this.data[index] = value;    // 对比当前节点与其父节点,如果当前节点更小就交换它们    while (index > 0 && this.compare(value, this.data[Math.floor((index - 1) / 2)]) = this.data.length) break;      // 没有右子节点      if (right >= this.data.length) {        this.data[i] = this.data[left];        i = left;        break;      }      // 比较左右子节点的大小,更小的补到父节点      if (this.compare(this.data[left], this.data[right])  b - a);maxHeap.insert(10);maxHeap.insert(4);maxHeap.insert(9);maxHeap.insert(1);maxHeap.insert(7);maxHeap.insert(5);maxHeap.insert(3);maxHeap.printHeap();maxHeap.delete(5);maxHeap.printHeap();maxHeap.delete(2);maxHeap.printHeap();

Python 版本

Pythonimport sys class BinaryHeap:       def __init__(self, capacity):         self.capacity = capacity         self.size = 0        self.Heap = [0]*(self.capacity + 1)         self.Heap[0] = -1 * sys.maxsize         self.FRONT = 1      def parent(self, pos):         return pos//2      def leftChild(self, pos):         return 2 * pos       def rightChild(self, pos):         return (2 * pos) + 1      def isLeaf(self, pos):         if pos >= (self.size//2) and pos <= self.size:             return True        return False      def swap(self, fpos, spos):         self.Heap[fpos], self.Heap[spos] = self.Heap[spos], self.Heap[fpos]       def heapifyDown(self, pos):           if not self.isLeaf(pos):             if (self.Heap[pos] > self.Heap[self.leftChild(pos)] or                self.Heap[pos] > self.Heap[self.rightChild(pos)]):                   if self.Heap[self.leftChild(pos)] = self.capacity :             return        self.size+= 1        self.Heap[self.size] = element           current = self.size           while self.Heap[current] 

高频题目

  • 剑指 Offer 40. 最小的k个数
  • 239. 滑动窗口最大值
  • 剑指 Offer 49. 丑数
  • 347. 前 K 个高频元素
  • HeapSort : https://www.geeksforgeeks.org/heap-sort/

总结

如果只是说堆什么?那么它是一个抽象的数据结构,表示可以非常迅速地拿到一堆数里面的最大值或者最小值,它并不少二叉堆,那么二叉堆和其他的各种堆,维基百科里面有详细说明:https://en.wikipedia.org/wiki/Heap_(data_structure)

注意:二叉堆只是堆的一种实现形式,二叉堆为什么出现得多?是因为它较为常见且简单,但是它并不是最优的实现,正是因为这个原因,所以二叉堆很多时候并不是完全的那么实用,那么如果在工程的代码里面我们可以直接调 优先队列(priority_queue) 就行了。

文章持续更新,可以公众号搜一搜「 一角钱技术 」第一时间阅读, 本文 GitHub org_hejianhui/JavaStudy 已经收录,欢迎 Star。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
// BTree.cpp : Defines the entry point for the console application. /* 作者:成晓旭 时间:2001年7月2日(9:00:00-14:00:00) 内容:完成二叉树的创建、前序遍历、中序遍历、后序遍历 时间:2001年7月2日(14:00:00-16:00:00) 内容:完成二叉树的叶子节点访问,交换左、右孩子 */ #include "stdafx.h" #include "stdlib.h" #define MAX_NODE 100 #define NODE_COUNT1 8 #define NODE_COUNT2 15 int TreeValue0[NODE_COUNT1][2] = {{'0',0},{'D',1},{'B',2},{'F',3},{'A',4},{'C',5},{'E',6},{'G',7}}; int TreeValue1[NODE_COUNT1][2] = {{'0',0},{'A',1},{'B',2},{'C',3},{'D',4},{'E',5},{'F',6},{'G',7}}; int TreeValue2[NODE_COUNT2][2] = {{'0',0},{'A',1},{'B',2},{'C',3},{'D',4},{'E',5},{'F',6},{'G',7},{'H',8},{'I',9},{'J',10},{'K',11},{'L',12},{'M',13},{'N',14}}; struct BTree { int data; int order; BTree *lchild; BTree *rchild; }; void Swap(int *p1,int *p2) { int t; t = *p1; *p1 = *p2; *p2 = t; } /* function CreateBTree()功能:创建一颗二叉树,并返回一个指向其根的指针 */ BTree *CreateBTree(int data[][2],int n) { BTree *Addr[MAX_NODE]; BTree *p, *head; int nodeorder,//节点序号 noderoot, //节点的双亲 i; if(n>MAX_NODE) { printf("参数错误!\n"); return(0); } for(i=1;i<=n;i++) { p = (BTree *)malloc(sizeof(BTree)); if(p==NULL) { printf("内存溢出错误!\n"); return(0); } else { p->data = data[i][0]; p->lchild = NULL; p->rchild = NULL; nodeorder = data[i][1]; p->order = nodeorder; Addr[nodeorder] = p; if(nodeorder>1) { noderoot = nodeorder/2; if(nodeorder %2 == 0) Addr[noderoot]->lchild = p; else Addr[noderoot]->rchild = p; } else head = p; printf("BTree[%d] = %c\t",p->order,p->data); } //free(p); } return(head); } /* function FirstOrderAccess0()功能:实现二叉树的前序遍历 二叉树前序遍历的思想: 从根节点开始,沿左子树一直走到没有左孩子的节点为止, 依次访问所经过的节点,同时所经[节点]的地址进栈; 当找到没有左孩子的节点时,从栈顶退出该节点的双亲的 右孩子,此时,此节点的左子树已访问完毕; 在用上述方法遍历该节点的右子树,如此重复到栈空为止。 */ void FirstOrderAccess0(BTree * header) { BTree * stack[MAX_NODE]; BTree *p; int top; top = 0; p = header; do { while(p!=NULL) { printf("BTree[%d] = %c\t",p->order,p->data);//访问节点P top = top+1; stack[top] = p; p = p->lchild;//继续搜索节点P的左子树 } if(top!=0) { p = stack[top]; top = top-1; p = p->rchild;//继续搜索节点P的右子树 } }while((top!=0)||(p!=NULL)); } /* function FirstOrderAccess1()功能:实现二叉树的前序遍历 二叉树前序遍历的思想: 从根节点开始,沿左子树一直走到没有左孩子的节点为止, 依次访问所经过的节点,同时所经[节点的非空右孩子]进栈; 当找到没有左孩子的节点时,从栈顶退出该节点的双亲的 右孩子,此时,此节点的左子树已访问完毕; 在用上述方法遍历该节点的右子树,如此重复到栈空为止。 */ void FirstOrderAccess1(BTree * header) { BTree * stack[MAX_NODE]; BTree *p; int top; top = 0; p = header; do { while(p!=NULL) { printf("BTree[%d] = %c\t",p->order,p->data); if(p->rchild!=NULL) stack[++top] = p->rchild; p = p->lchild; } if(top!=0) p = stack[top--]; }while((top>0)||(p!=NULL)); } /* function MiddleOrderAccess()功能:实现二叉树的中序遍历 二叉树中序遍历的思想: 从根节点开始,沿左子树一直走到没有左孩子的节点为止, 并将所经[节点]的地址进栈; 当找到没有左孩子的节点时,从栈顶退出该节点并访问它, 此时,此节点的左子树已访问完毕; 在用上述方法遍历该节点的右子树,如此重复到栈空为止。 */ void MiddleOrderAccess(BTree * header) { BTree * stack[MAX_NODE]; BTree *p; int top; top = 0; p = header; do { while(p!=NULL) { stack[++top] = p;//节点P进栈 p = p->lchild; //继续搜索其左子树 } if(top!=0) { p = stack[top--];//节点P出栈 printf("BTree[%d] = %c\t",p->order,p->data);//访问节点P p = p->rchild;//继续搜索其左子树 } }while((top!=0)||(p!=NULL)); } /* function LastOrderAccess()功能:实现二叉树的后序遍历 二叉树后序遍历的思想: 从根节点开始,沿左子树一直走到没有左孩子的节点为止, 并将所经[节点]的地址第一次进栈; 当找到没有左孩子的节点时,此节点的左子树已访问完毕; 从栈顶退出该节点,判断该节点是否为第一次进栈,如是,再 将所经[节点]的地址第二次进栈,并沿该节点的右子树一直走到 没有右孩子的节点为止,如否,则访问该节点;此时,该节点的 左、右子树都已完全遍历,且令指针p = NULL; 如此重复到栈空为止。 */ void LastOrderAccess(BTree * header) { BTree * stack[MAX_NODE];//节点的指针栈 int count[MAX_NODE];//节点进栈次数数组 BTree *p; int top; top = 0; p = header; do { while(p!=NULL) { stack[++top] = p;//节点P首次进栈 count[top] = 0; p = p->lchild; //继续搜索节点P的左子树 } p = stack[top--];//节点P出栈 if(count[top+1]==0) { stack[++top] = p;//节点P首次进栈 count[top] = 1; p = p->rchild; //继续搜索节点P的左子树 } else { printf("BTree[%d] = %c\t",p->order,p->data);//访问节点P p = NULL; } }while((top>0)); } /* function IsLeafNode()功能:判断给定二叉树的节点是否是叶子节点 */ int IsLeafNode(BTree *node) { if((node->lchild==NULL)&&(node->rchild==NULL)) return(1); else return(0); } /* function PrintLeafNode()功能:输出给定二叉树的叶子节点 */ void PrintLeafNode(BTree *header) { BTree * stack[MAX_NODE];//节点的指针栈 BTree *p; int top; p = header; top = 0; do { while(p!=NULL) { stack[++top] = p; p = p->lchild;//继续搜索节点P的左子树 } if(top!=0) { p = stack[top--]; if(IsLeafNode(p)) printf("LNode[%d] = %c\t",p->order,p->data);//访问叶子节点 p = p->rchild;//继续搜索节点P的右子树 } }while(top>0||p!=NULL); } /* function HasTwoChildNode()功能:判断给定二叉树的节点是否存在两个孩子节点 */ int HasTwoChildNode(BTree *node) { if((node->lchild!=NULL)&&(node->rchild!=NULL)) return(1); else return(0); } /* function SwapChildNode()功能:交换给定二叉树的所有节点的左、右孩子 */ void SwapChildNode(BTree *header) { BTree * stack[MAX_NODE];//节点的指针栈 BTree *p; int top; p = header; top = 0; do { while(p!=NULL) { stack[++top] = p; p = p->lchild;//继续搜索节点P的左子树 } if(top!=0) { p = stack[top--]; if(HasTwoChildNode(p)) Swap(&p->lchild->data,&p->rchild->data);//交换节点P的左、右孩子 p = p->rchild;//继续搜索节点P的右子树 } }while(top>0||p!=NULL); } int main(int argc, char* argv[]) { BTree * TreeHeader; printf("二叉树创建数据结果:\n"); TreeHeader = CreateBTree(TreeValue1,NODE_COUNT1-1); //TreeHeader = CreateBTree(TreeValue2,NODE_COUNT2-1); if (TreeHeader==0) { printf("二叉树创建失败!\n"); return(0); } else { printf("\n二叉树前序遍历结果:\n"); FirstOrderAccess1(TreeHeader); printf("\n二叉树中序遍历结果:\n"); MiddleOrderAccess(TreeHeader); printf("\n二叉树后序遍历结果:\n"); LastOrderAccess(TreeHeader); //printf("\n二叉树的所有叶子节点:\n"); //PrintLeafNode(TreeHeader); //SwapChildNode(TreeHeader); //printf("\n二叉树交换孩子的结果:\n"); //MiddleOrderAccess(TreeHeader); printf("\n程序运行完毕!\n"); return 0; } }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值