【数据结构与算法】二叉树(上)

简介: 【数据结构与算法】二叉树(上)

👉树的概念及结构👈


树的概念


树是一种非线性的数据结构,它是由n(n >= 0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的

有一个特殊的结点,称为根结点,根节点没有前驱结点。

除根节点外,其余结点被分成M(M>0)个互不相交的集合 T1、T2、……、Tm,其中每一个集合 Ti (1<= i <= m) 又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有 0 个或多个后继。因此,树是递归定义的。

7724752305064138b945f912fdcca4fe.png

注意:树形结构中,子树之间不能有交集,否则就不是树形结构


cdbc133ec18e4ccd944ec9a30749a986.png


如果子树有交集,那么这个结构就不再是树,而是更加高级的数据结构 - - 图


树的相关概念


113eeef0ad3a408c96aa69ab3cf71af6.png


节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A 的为 6


叶节点或终端节点:度为 0 的节点称为叶节点; 如上图:B、C、H、I 等节点为叶节点


非终端节点或分支节点:度不为 0 的节点; 如上图:D、E、F、G 等节点为分支节点


双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A 是 B 的父节点


孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B 是 A 的孩子节点


兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C 是兄弟节点


树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为 6


节点的层次:从根开始定义起,根为第 1 层,根的子节点为第 2 层,以此类推;


树的高度或深度:树中节点的最大层次; 如上图:树的高度为 4


堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I 互为兄弟节点


节点的祖先:从根到该节点所经分支上的所有节点;如上图:A 是所有节点的祖先


子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙


森林:由 m(m>0)棵互不相交的树的集合称为森林(并查集)


以上有关于树的概念,是在树加上人类亲属关系的基础上建立起来的。知道了这些,我们现在来学一下树的表示。


树的表示


树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间
的关系
,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法

等。

如果知道了树的度为N,我们可以像下面这样来定义树。


#define N 5
struct TreeNode
{
  int data;
  struct TreeNode* childArr[N];
  int childSize; // 孩子个数
}


虽然树向上面那样定义是可以的,但是比较浪费空间。因为不是每个节点的都都为N。有人又想出了另一种定义树的方法。如下图代码所示:


struct TreeNode
{
  int data;
  // 顺序表存储孩子节点指针
  struct TreeNode** childArr;
  int childSize; // 孩子个数
  int childCapacity; // 孩子容量
}


以上的定义方式可以做到,你想要多少空间就就动态申请多少空间。不过这种树也是相当地复杂,又有人想出更加厉害的定义树的方法,称之为左孩子右兄弟表示法


左孩子右兄弟表示法


typedef int TDataType;
struct Node
{
  struct Node* firstChild1; // 左孩子
  struct Node* pNextBrother; // 右兄弟
  TDataType data;
};

be569811c538409fadea0db3231fe4df.png

左孩子右兄弟表示法就是父亲指向左边第一个孩子,孩子之间用兄弟指针链接起来。


双亲表示法

9e9613d587a64d899aaaa4b69eb25beb.png

树在实际中的运用


表示文件系统的目录树结构

a133c151346b4126871ccd31c7fa149b.png

👉二叉树概念及结构👈


概念


一棵二叉树是结点的一个有限集合,该集合为空或者由一个根节点加上两棵被称为左子树和右子树的二叉树组成

e83eb4900235489c8e77128f2cd8f897.png

从上图可以看出:

  1. 二叉树不存在度大于 2 的结点
  2. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

注意:对于任意的二叉树都是由以下几种情况复合而成的

cc69945f15e3453b9f06c39efae8b67c.png



特殊的二叉树


满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是 说,如果一个二叉树的层数为 K,且结点总数是 2 K 2^{K}2

K

 - 1,则它就是满二叉树。

完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为 K 的,有 n 个结点的二叉树,当且仅当其每一个结点都与深度为 K 的满二叉树中编号从 1 至 n 的结点一一对应时称之为完全二叉树。要注意的是满二叉树是一种特殊的完全二叉树。

98c97ee77ffd426c97a5cbbcc424b60a.png


现实中的二叉树


28198753ed914f9fa751d66beb81942b.png


二叉树的性质


若规定根节点的层数为 1,则一棵非空二叉树的第 i 层上最多有 2 i − 1 2^{i - 1}2

i−1

 个结点

若规定根节点的层数为 1,则深度为 h 的二叉树的最大结点数是 2 h 2^{h}2

h

 - 1

对任何一棵二叉树, 如果度为 0 的叶结点个数为 n0, 度为 2 的分支结点个数为 n2,则有 n0= n2 + 1

若规定根节点的层数为 1,具有 n 个结点的满二叉树的深度 h = . l o g 2 n log_2{n}log

2

n

对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从 0 开始编号,则对于序号为 i 的结点有:

若 i > 0,i 位置节点的双亲序号为 (i - 1) / 2;i = 0时,i 为根节点编号,无双亲节点

若2i + 1 < n,左孩子序号为2i + 1。当2i + 1 >= n时,则该节点无左孩子

若2i + 2 < n,右孩子序号为2i + 2,当2i + 2 >= n时,则该节点无右孩子


现在我们来做一道选择题,加深对上面的概念和性质的理解。


在具有2n个结点的完全二叉树中,叶子结点个数为( )

A. n

B. n+1

C. n-1

D. n/2

f3826070450142cdb0f7c742501d2514.png


二叉树的存储结构


二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。

  • 顺序存储
  • 顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。


b29eb1f3c43141e4974c6528a7f90c1f.png


链式存储

二叉树的链式存储结构是指用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面学到高阶数据结构如红黑树等会用到三叉链。

bb8665adfd784c7aa11ee181b702f0c3.png

6692bfd86c4e4b03875bff70fa0e8c46.png



👉二叉树的顺序结构及实现👈


二叉树的顺序结构


普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种完全二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

d2ae694c48a84c88a98adf7c523ed9b5.png

堆的概念及结构


2a7ec4da18dd4977870e5d4ca8873540.png

堆的性质:

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树

b211aafb04834f38a0f0fbdd664de03b.png

一道选择题


1.下列关键字序列为堆的是(A

A 100,60,70,50,32,65

B 60,70,65,50,32,100

C 65,100,70,32,50,60

D 70,65,100,32,50,60

E 32,50,100,70,65,60

F 50,100,70,65,60,32

c8375c7e864642d99178d91598a58f36.png

堆的实现


堆是一种完全二叉树,我们可以使用顺序表来存储堆的数据。堆要实现的函数接口有初始化堆、销毁堆、打印堆、插入数据 x 并保持堆的形态、删除堆顶的数据、返回堆顶数据、判断堆是否为空以及堆中数据的个数。


1.Heap.h


#pragma once
#include  <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
  HPDataType* a;
  int size;
  int capacity;
}HP;
void HeapInit(HP* php);
void HeapDestroy(HP* php);
void HeapPrint(HP* php);
// 插入数据x并保持堆的形态
void HeapPush(HP* php, HPDataType x);
// 删除堆顶的元素  -- 找次大或次小的数
// log(N)
void HeapPop(HP* php);
// 返回堆顶元素
HPDataType HeapTop(HP* php);
bool HeapEmpty(HP* php);
int HeapSize(HP* php);


2.Heap.c


#include "Heap.h"
// 初始化堆
void HeapInit(HP* php)
{
  assert(php);
  php->a = NULL;
  php->size = php->capacity = 0;
}
// 销毁堆
void HeapDestroy(HP* php)
{
  assert(php);
  free(php->a);
  php->size = php->capacity = 0;
}
// 打印堆
void HeapPrint(HP* php)
{
  assert(php);
  for (int i = 0; i < php->size; i++)
  {
    printf("%d ", php->a[i]);
  }
  printf("\n");
}
// 交换堆的数据
void Swap(HPDataType* p1, HPDataType* p2)
{
  HPDataType tmp = *p1;
  *p1 = *p2;
  *p2 = tmp;
}
// 向上调整算法
void AdjustUp(HPDataType* a, int child)
{
  int parent = (child - 1) / 2;
  while (child > 0)
  {
    if (a[child] < a[parent])
    {
      Swap(&a[child], &a[parent]);
      child = parent;
      parent = (child - 1) / 2;
    }
    else
    {
      break;
    }
  }
}
// 插入数据x并保持堆的形态
void HeapPush(HP* php, HPDataType x)
{
  assert(php);
  if (php->size == php->capacity)
  {
    int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
    HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity * sizeof(HPDataType));
    if (tmp == NULL)
    {
      perror("realloc fail");
      exit(-1);
    }
    php->a = tmp;
    php->capacity = newCapacity;
  }
  php->a[php->size] = x;
  php->size++;
  AdjustUp(php->a, php->size - 1);
}
// 向下调整算法
// n为堆中元素的个数
void AdjustDown(HPDataType* a, int n, int parent)
{
  int minChild = 2 * parent + 1;
  while (minChild < n)
  {
    if (minChild + 1 < n && a[minChild + 1] < a[minChild])// 有左孩子且左孩子的值小于右孩子的值
    {
      minChild++;
    }
    if (a[minChild] < a[parent])
    {
      Swap(&a[minChild], &a[parent]);
      parent = minChild;
      minChild = 2 * parent + 1;
    }
    else
    {
      break;
    }
  }
}
// 删除堆顶的元素  -- 找次大或次小的数
// log(N)
void HeapPop(HP* php)
{
  assert(php);
  assert(!HeapEmpty(php));
  Swap(&php->a[0], &php->a[php->size - 1]);
  php->size--;
  AdjustDown(php->a, php->size, 0);
}
// 返回堆顶元素
HPDataType HeapTop(HP* php)
{
  assert(php);
  assert(!HeapEmpty(php));
  return php->a[0];
}
// 判断堆是否为空
bool HeapEmpty(HP* php)
{
  assert(php);
  return php->size == 0;
}
// 堆中数据的个数
int HeapSize(HP* php)
{
  assert(php);
  return php->size;
}


插入数据x并保持堆的形态


1.判断是否需要扩容

2.插入数据

3.使用向上调整算法保持堆的形态


// 插入数据x并保持堆的形态
void HeapPush(HP* php, HPDataType x)
{
  assert(php);
  if (php->size == php->capacity)
  {
    int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
    HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity * sizeof(HPDataType));
    if (tmp == NULL)
    {
      perror("realloc fail");
      exit(-1);
    }
    php->a = tmp;
    php->capacity = newCapacity;
  }
  php->a[php->size] = x;
  php->size++;
  AdjustUp(php->a, php->size - 1);
}


向上调整算法


// 向上调整算法
void AdjustUp(HPDataType* a, int child)
{
  int parent = (child - 1) / 2;
  while (child > 0)
  {
    if (a[child] < a[parent])
    {
      Swap(&a[child], &a[parent]);
      child = parent;
      parent = (child - 1) / 2;
    }
    else
    {
      break;
    }
  }
}

66b6e507b71d4354b3c25a8a69f68d26.png



以小根堆为例,大根堆同理。小根堆向上调整算法:如果插入的数据x小于父节点的数据,那么就交换x和父节点的数据,然后更新子节点的下标child和父节点的下标parent,以此类推直至数据x大于父节点的数据或者child <= 0。


删除堆顶的数据


1.判断堆是否为空

2.交换堆顶的数据和最后一个数据

3.删除堆顶的数据php->size--

4.使用向下调整算法保持堆的形态


// 删除堆顶的元素  -- 找次大或次小的数
// log(N)
void HeapPop(HP* php)
{
  assert(php);
  assert(!HeapEmpty(php));
  Swap(&php->a[0], &php->a[php->size - 1]);
  php->size--;
  AdjustDown(php->a, php->size, 0);
}


向下调整算法


使用向下调整算法的前提:左右子树必须是一个堆,才能调整。也就是说除了根节点不满足堆,因此需要从根节点开始向下调整为堆。


// n为堆中元素的个数
void AdjustDown(HPDataType* a, int n, int parent)
{
  int minChild = 2 * parent + 1;
  while (minChild < n)
  {
    if (minChild + 1 < n && a[minChild + 1] < a[minChild])// 有左孩子且左孩子的值小于右孩子的值
    {
      minChild++;
    }
    if (a[minChild] < a[parent])
    {
      Swap(&a[minChild], &a[parent]);
      parent = minChild;
      minChild = 2 * parent + 1;
    }
    else
    {
      break;
    }
  }
}

ffad6dd946de4eed96fd3fa45014a778.png


以小根堆为例,大根堆同理。小根堆向下调整算法:创建变量minChild,记录较小孩子的下标并赋初始值为2 * parent + 1。若有右孩子且右孩子的值小于左孩子的值,则minChild++。当孩子的值小于父节点的值时,交换孩子的值和父节点的值,并更新子节点的下标minChild和父节点的下标parent。;当孩子的值大于父节点的值或者子节点的下标minChild大于堆中数据的个数n时,退出while循环,向下调整结束。


3.Test.c


以下为函数接口的测试代码,大家可以参考一下。


#include "Heap.h"
int main()
{
  int a[] = { 15, 18, 19, 25, 28, 34, 65, 49, 27, 37 };
  //int a[] = { 65, 100, 70, 32, 50, 60 };
  HP hp;
  HeapInit(&hp);
  for (int i = 0; i < sizeof(a) / sizeof(int); ++i)
  {
    HeapPush(&hp, a[i]);
  }
  HeapPrint(&hp);
  HeapPush(&hp, 10);
  HeapPrint(&hp);
  //HeapPop(&hp);
  //HeapPrint(&hp);
  //HeapPop(&hp);
  //HeapPrint(&hp);
  while (!HeapEmpty(&hp))
  {
    printf("%d ", HeapTop(&hp));
    HeapPop(&hp);
  }
  return 0;
}

21436108fec34a9eae8f2c5644ae615b.jpg


👉堆的应用👈


堆的创建


通过Test.c源文件的代码,我们可以看到,我们是将数组a中的数据全部插入到堆中,才建成堆的。而不是直接将数组a建成一个堆,那这样的建堆方式似乎过于麻烦。因为我们还需要将堆的实现代码写出来,才能建成堆。为了解决这个问题,我们现在就来学习堆的创建。以建小根堆为例,建大根堆同理。


1.向上调整建堆


将数组第一个元素看成一个堆,然后利用for循环模拟数据插入的过程向上调整建堆。向上调整建堆的时间复杂度为O(N*logN)


#include <stdio.h>
void Swap(int* x, int* y)
{
  int tmp = *x;
  *x = *y;
  *y = tmp;
}
// 向上调整算法
void AdjustUp(int* a, int child)
{
  int parent = (child - 1) / 2;
  while (child > 0)
  {
    if (a[child] < a[parent])
    {
      Swap(&a[child], &a[parent]);
      child = parent;
      parent = (child - 1) / 2;
    }
    else
    {
      break;
    }
  }
}
// 堆排序
void HeapSort(int* a, int size)
{
  // 建堆 -- 向上调整建堆 - O(N*logN)
  // for循环模拟数据插入的过程向上调整
  for (int i = 1; i < size; i++)
  {
    AdjustUp(a, i);
  }
  for (int i = 0; i < size; i++)
  {
    printf("%d ", a[i]);
  }
}
int main()
{
  int a[] = { 15,1,19,25,8,34,65,4,27,7 };
  int sz = sizeof(a) / sizeof(a[0]);
  HeapSort(a, sz);
  return 0;
}

c5908b30945b41b887e6a39c54114197.png

2.向下调整建堆


使用向下调整算法的前提是左右子树都是堆,那么我们就从倒数第一个非叶子节点(最后一个节点的父节点)开始向下调整,直到调整到根。向下调整建堆的时间复杂度为O(N)。


#include <stdio.h>
void Swap(int* x, int* y)
{
  int tmp = *x;
  *x = *y;
  *y = tmp;
}
// 向下调整算法
void AdjustDown(int* a, int size, int parent)
{
  int minChild = 2 * parent + 1;
  while (minChild < size)
  {
    // 右孩子存在且右孩子小于左孩子
    if (minChild + 1 < size && a[minChild + 1] < a[minChild])
    {
      minChild++;
    }
    if (a[minChild] < a[parent])
    {
      Swap(&a[minChild], &a[parent]);
      parent = minChild;
      minChild = 2 * parent + 1;
    }
    else
    {
      break;
    }
  }
}
// 堆排序
void HeapSort(int* a, int size)
{
  // 建堆 -- 向下调整建堆 - O(N)
  // 从倒数第一个非叶节点(最后一个节点的父亲)开始向下调整直到调整到根节点
  for (int i = (size - 1 - 1) / 2; i >= 0; i--)
  {
    AdjustDown(a, size, i);
  }
  for (int i = 0; i < size; i++)
  {
    printf("%d ", a[i]);
  }
}
int main()
{
  int a[] = { 15,1,19,25,8,34,65,4,27,7 };
  int sz = sizeof(a) / sizeof(a[0]);
  HeapSort(a, sz);
  return 0;
}

060ab69c69ef4818bcce247b562213e9.png

cbf97ab191284241bc04c24d2f72369b.png

3.建堆的实现复杂度证明


因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的

就是近似值,多几个节点不影响最终结果)。向上调整建堆的时间复杂度为O(N*logN),向下调整建堆的时间复杂度为O(N)。


向上调整建堆


0195153b39f84f349e307ed0da4f6357.png


向下调整建堆

bba9833a60384a12b2dccb7efb99bcf5.png


堆排序


堆排序即利用堆的思想来进行排序,总共分为两个骤:

建堆

升序:建大堆

降序:建小堆

利用堆删除思想来进行排序

建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

613574ae917e43b9a521dc3213753cd4.png


以升序为例,降序同理。升序建大根堆,堆顶的数据跟最后位置的数据交换,把最后位置的数据不看做堆里面的数据,向下调整,选出次大的数据,依次类推就能实现升序。


#include <stdio.h>
void Swap(int* x, int* y)
{
  int tmp = *x;
  *x = *y;
  *y = tmp;
}
// 向下调整算法
void AdjustDown(int* a, int size, int parent)
{
  int minChild = 2 * parent + 1;
  while (minChild < size)
  {
    // 右孩子存在且右孩子小于左孩子
    if (minChild + 1 < size && a[minChild + 1] > a[minChild])
    {
      minChild++;
    }
    if (a[minChild] > a[parent])
    {
      Swap(&a[minChild], &a[parent]);
      parent = minChild;
      minChild = 2 * parent + 1;
    }
    else
    {
      break;
    }
  }
}
// 堆排序
void HeapSort(int* a, int size)
{
  // 大思路:选择排序,依次选数,从后往前排
  // 升序 -- 建大根堆
  // 降序 -- 建小根堆
  // 建堆 -- 向下调整建堆 - O(N)
  // 从倒数第一个非叶节点(最后一个节点的父亲)开始向下调整直到调整到根节点
  for (int i = (size - 1 - 1) / 2; i >= 0; i--)
  {
    AdjustDown(a, size, i);
  }
  // 选数
  int i = 1;
  while (i < size)
  {
    Swap(&a[0], &a[size - i]);
    AdjustDown(a, size - i, 0);
    i++;
  }
  for (i = 0; i < size; i++)
  {
    printf("%d ", a[i]);
  }
}
int main()
{
  int a[] = { 15,1,19,25,8,34,65,4,27,7 };
  int sz = sizeof(a) / sizeof(a[0]);
  HeapSort(a, sz);
  return 0;
}

e78ac408f2654c81af0d7b035ec2dc07.png


堆排序的时间复杂度为O(N*logN),相较于O(N^2)的排序算法已经快了很多。比如:用O(N*logN)和O(N^2)的排序算法来排序 100w 个数据,看看它们的效率差别。

372e14788b944639a46818151e0ed8b4.png


TOP-K问题


TOP-K 问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大

比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。



对于 Top-K 问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:


用数据集合中前 K 个元素来建堆

前K个最大的元素,则建小堆

前K个最小的元素,则建大堆

用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素,将剩余 N-K 个元素依次与堆顶元素比完之后,堆中剩余的 K 个元素就是所求的前K 个最小或者最大的元素。


762a1de971ba485db6e81115c00f1d6c.png

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <assert.h>
// 创建1w个数据的文件
void CreateDataFile(const char* filename, int N)
{
  FILE* fin = fopen(filename, "w");
  if (fin == NULL)
  {
    perror("fopen fail");
    return;
  }
  srand((unsigned int)time(NULL));
  for (int i = 0; i < N; ++i)
  {
    fprintf(fin, "%d\n", rand() % 30000); // 生成N个小于30000的数据
  }
  fclose(fin);
  fin = NULL;
}
// 选出前K大的数
void PrintTopK(const char* filename, int K)
{
  assert(filename);
  FILE* fout = fopen(filename, "r");
  if (fout == NULL)
  {
    perror("fopen fail");
    return;
  }
  int* minHeap = (int*)malloc(sizeof(int) * K); 
  if (minHeap == NULL)
  {
    perror("malloc fail");
    return;
  }
  // 如何读取前K个数据
  for (int i = 0; i < K; ++i)
  {
    fscanf(fout, "%d", &minHeap[i]);
  }
  // 建K个数小堆
  for (int j = (K - 2) / 2; j >= 0; --j)
  {
    AdjustDown(minHeap, K, j);
  }
  // 继续读取后N-K个数据
  int val = 0;
  while (fscanf(fout, "%d", &val) != EOF)
  {
    if (val > minHeap[0])
    {
      minHeap[0] = val;
      AdjustDown(minHeap, K, 0);
    }
  }
  for (int i = 0; i < K; ++i)
  {
    printf("%d ", minHeap[i]);
  }
  free(minHeap);
  fclose(fout);
  fout = NULL;
}
int main()
{
  const char* filename = "Data.txt";
  int N = 10000;
  int K = 10;
  CreateDataFile(filename, N);
  PrintTopK(filename, K);
  return 0;
}

76fb46aa64ec402a8ffc2ed9db451409.png


上面的程序,利用随机数生成器(srand函数只会生成0~32767的数字)在Data.txt文件中生成了 1w 个小于 30000 的数据,然后选出 10 大的数据。那我们该如何验证这个程序的正误呢?我们可以在Data.txt文件加上几个大于 30000 的数据,来验证程序的正误。 注意:修改了Data.txt文件后,记得将CreateDataFile函数屏蔽掉,否则Data.txt文件里的数据会被刷新。

6c1e411fb7e147e78aa142db4803410d.png

3f091540aa3b4b22a5d8b6282e35bbe1.png


👉总结👈


本篇博客主要讲解了树和二叉树的基本概念、二叉树的性质、堆的实现、堆排序已经TOP-K问题。以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家啦!💖💝❣️






相关文章
|
26天前
|
算法
分享一些提高二叉树遍历算法效率的代码示例
这只是简单的示例代码,实际应用中可能还需要根据具体需求进行更多的优化和处理。你可以根据自己的需求对代码进行修改和扩展。
|
1月前
|
存储 缓存 算法
如何提高二叉树遍历算法的效率?
选择合适的遍历算法,如按层次遍历树时使用广度优先搜索(BFS),中序遍历二叉搜索树以获得有序序列。优化数据结构,如使用线索二叉树减少空指针判断,自定义节点类增加辅助信息。利用递归与非递归的特点,避免栈溢出问题。多线程并行遍历提高速度,注意线程安全。缓存中间结果,避免重复计算。预先计算并存储信息,提高遍历效率。综合运用这些方法,提高二叉树遍历算法的效率。
50 5
|
1月前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
111 8
|
1月前
|
机器学习/深度学习 JSON 算法
二叉树遍历算法的应用场景有哪些?
【10月更文挑战第29天】二叉树遍历算法作为一种基础而重要的算法,在许多领域都有着不可或缺的应用,它为解决各种复杂的问题提供了有效的手段和思路。随着计算机科学的不断发展,二叉树遍历算法也在不断地被优化和扩展,以适应新的应用场景和需求。
32 0
|
2月前
|
存储 算法 关系型数据库
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
这篇文章主要介绍了多路查找树的基本概念,包括二叉树的局限性、多叉树的优化、B树及其变体(如2-3树、B+树、B*树)的特点和应用,旨在帮助读者理解这些数据结构在文件系统和数据库系统中的重要性和效率。
26 0
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
|
2月前
|
存储 算法 搜索推荐
数据结构与算法学习十七:顺序储存二叉树、线索化二叉树
这篇文章主要介绍了顺序存储二叉树和线索化二叉树的概念、特点、实现方式以及应用场景。
28 0
数据结构与算法学习十七:顺序储存二叉树、线索化二叉树
|
2月前
|
Java
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(二)
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(二)
29 1
|
2月前
|
算法 Java C语言
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(一)
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(一)
26 1
|
2月前
|
存储
【数据结构】二叉树链式结构——感受递归的暴力美学
【数据结构】二叉树链式结构——感受递归的暴力美学
|
2月前
|
存储 算法
【二叉树】—— 算法题
【二叉树】—— 算法题
【二叉树】—— 算法题