一个完整的B+树代码及详细注释

本文档介绍了一个使用B+树构建文件系统时间索引的简单实现,目的是支持对时间点和时间段的快速检索。作者修复了源代码中的错误,确保每个节点的正确连接,并提供了插入、搜索和删除操作。代码中还包括了并发控制的锁机制,以及文件存储和初始化的细节。示例展示了文件初始化和插入数据后的文件大小变化。
摘要由CSDN通过智能技术生成

因为这一段在开发自己的文件系统,需要支持时间点和时间段的快速检索,所以考虑使用B+树构建时间索引。github上有不少B+树的源码,但是都比较复杂。这里找到一个非常简单的代码实现,代码设计的非常精妙,但是没有注释。连续看了几天,结合我自己对B+树的原理的理解,给代码增加了注释,并修复了代码中的bug(源代码有误,会在平级的内部节点之间形成链,从而导致部分中间节点和叶子节点没有父节点,造成检索失败)。后面我会基于此修改成满足项目需求的子模块。在这里先把代码贴出来吧。如果有问题,请在评论区讨论。


/*
 * To conver to an append-only design, only the 'fwrite' in the '_insert' function
 * must be changed. All other writes (save for delete), are appended to the end.
 * Also, the root address must instead be written to and searched for at the end.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
//L_ctermid是一个和系统调用char * ctermid(char *)相关的宏,该值为一个int,其大小足够容纳ctermid()返回的字符串,
//也就是返回值char * 的长度不会超过L_ctermid,但是NOPE是没有的意思,所以这里可能表示没有定义L_ctermid.
#ifdef L_ctermidNOPE
#include <unistd.h>
#include <fcntl.h>
#define LOCK//定义了锁,增加了并发的操作可能
#endif

typedef unsigned long long uint64_t;
typedef unsigned int uint32_t;
typedef unsigned char uint8_t;
typedef struct db db;

#define SIZEOF_LONG sizeof(uint64_t)//表示一个地址占用的字节数(这里的地址就是在文件中的位置)
#define _HASH 33                    //一个node里面,每个项item所占用的字节数,也就是key所占用的字节数
#define _ORDER 99                   //阶数,可以理解为叉数
#define _WIDTH 1 + _HASH *_ORDER + SIZEOF_LONG *(_ORDER + 1) //每个node所占用的空间大小,1个字节为0/1表示是否是叶子节点,_ORDER个项占_ORDER*_HASH字节,(_ORDER+1)个指针占(_ORDER+1)*8;(地址数量比项数量多1,每个地址用8字节表示)
#define _DEPTH 10               //树的深度
#define _MAX 0xf4240            //1+e6,一百万(一个value的上限)

struct db//在fp表示的存储B+树的文件中,从根节点出发到达某个节点的路径上的节点和各节点的地址(在文件中的位置)
{
  FILE *fp;
  unsigned char path[_DEPTH][_WIDTH];
  uint64_t node_addrs[_DEPTH];
#ifdef LOCK
  struct flock fl;
#endif
};

void to_big(unsigned char *, uint64_t);
uint64_t from_big(unsigned char *);
void node_split(db *, int, unsigned char);
void _insert(db *, unsigned char *, int, uint64_t, uint64_t, int);
void put(db *, unsigned char *, unsigned char *);
uint64_t search(db *, unsigned char *, int *);
unsigned char *get(db *, unsigned char *);
void db_init(db *, const char *);

#ifdef LOCK
int db_lock(db *);
int db_unlock(db *);
#endif

void db_close(db *);

#ifdef LOCK
int db_lock(db *db)
{
  db->fl.l_type = F_WRLCK;
  db->fl.l_whence = SEEK_SET;
  db->fl.l_start = 0;
  db->fl.l_len = 0;
  db->fl.l_pid = getpid();
  return fcntl((db->fp)->_file, F_SETLKW, &(db->fl)); //给文件设置写锁
}

int db_unlock(db *db)
{
  db->fl.l_type = F_UNLCK;
  fcntl((db->fp)->_file, F_SETLK, &(db->fl)); //释放加在文件上的锁(读锁或者写锁)
}
#endif

/**
 * 将64位整数转换成大端表示形式,并放到数组中。这种方式是便于人理解的,高位在前,低位在后,
 * 便于统一比较和计算大小(主要用于key的比较,从前往后遍历即可确定谁大谁小)。
 * 例如:uint64_t val =1234567890在内存或者文件上的二进制表示为:d2,02,96,49,00,00,00,00;
 * 这是小端表示方式,也就是低位在前,高位在后,无法直接用于比较整数大小;如果换成大端表示,则为:
 * 00,00,00,00,49,96,02,d2,而用16进制表示1234567890这个数,也是0x499602d2.
 */
void to_big(unsigned char *buf, uint64_t val)
{
  int i;
  for (i = 0; i < sizeof(uint64_t); ++i)
    buf[i] = (val >> (56 - (i * 8))) & 0xff;
}

/**
 * 将使用大端形式来表示整数的一个无符号字符数组拼装成一个64位整数。
 */
uint64_t
from_big(unsigned char *buf)
{
  uint64_t val = 0;
  int i;
  for (i = 0; i < sizeof(uint64_t); ++i)
    val |= (uint64_t)buf[i] << (56 - (i * 8));
  return val;
}

/**
 * 处理节点分裂的情况;分成两种分裂:叶子节点分裂和内部节点分裂。注意:node_split()和_insert()是相互调用的,这是这套代码的精妙之处。
 */ 
void node_split(db *db, int index, unsigned char isleaf)
{
  unsigned char *node = db->path[index];//待分裂的节点
  unsigned char *lnode = malloc(_WIDTH + 1);//左子树的根节点(或者说左子节点)
  unsigned char *rnode = malloc(_WIDTH + 1);//右子树的根节点(或者说右子节点)
  memset(lnode, 0, _WIDTH + 1);
  memset(rnode, 0, _WIDTH + 1);

  int split = (_HASH + SIZEOF_LONG) * (_ORDER >> 1) + 1;//计算分裂点的位置
  memcpy(lnode, node, split);//将node中位于分裂点左侧的数据复制到lnode中,充当左子树的根节点
  rnode[0] = isleaf;//右子树的根节点的类型与待分裂节点一致。如果待分裂的节点是叶子节点,也由于分裂是向上生长,留下来的节点肯定还是叶子节点;
  //可以推测,左子树根节点的类型与右子树的根节点类型是一致的,这里只是没有单独给lnode[0]赋值。
  isleaf ? memcpy(rnode + 1, node + split, _WIDTH - split) : memcpy(rnode + 1, node + split + SIZEOF_LONG + _HASH, _WIDTH - (split + SIZEOF_LONG + _HASH));
  //如果是叶子节点,则中间的项会保留在右子树中,而不会分裂到上一层,只是把key复制到上层而已,本项仍保留在右子树中;如果是内部节点,则中间项是要提升到上层的。

  fseeko(db->fp, 0, SEEK_END);
  uint64_t addr = ftello(db->fp);
  fwrite(rnode, 1, _WIDTH, db->fp);//把右子树的根节点写入文件
 //叶子节点保留平级右指针,内部节点应该不用右指针
  //to_big(lnode + split, addr);//此代码错误,会在平级内部节点之间用指针链起来,造成部分内部节点和叶子节点没有父节点,造成无法检索。
  isleaf ? to_big(lnode + split, addr) : memcpy(lnode, node, split + SIZEOF_LONG); //将右子树根节点地址写入到左子树的最后一个有效位置,相当于把它们串起来了。不是只把叶子那层串起来吗?这里怎么把内部节点层也串起来了?
  //要升上去的项肯定是是放到内部节点中的,所以这里把待升上去的key抽出来,给其配对的左右指针分别指向lnode的地址和rnode的地址。
  
// to_big(lnode + split, addr);//将右子树根节点地址写入到左子树的最后一个有效位置,相当于把它们串起来了。不是只把叶子那层串起来吗?这里怎么把内部节点层也串起来了?
  //要升上去的项肯定是是放到内部节点中的,所以这里先把待升上去的项抽出来,主要是key和指向value的地址啦
  unsigned char *key = malloc(_HASH + 1);
  memset(key, 0, _HASH + 1);
  memcpy(key, node + split + SIZEOF_LONG, _HASH);
  memcpy(db->path[index], lnode, _WIDTH);//db->path[index]用左子树根节点代替
  if (index > 0)
  {
    _insert(db, key, index - 1, db->node_addrs[index], addr, 0);//把key和记录对应内容的地址db->node_addrs[index]构成的项,添加到上一层的node中,这一项对应的节点为内部节点,该项的右子树地址为addr
  }
  else//index==0说明是第一次分裂,也就是最早的根节点填充满了,需要分裂,由最初的一层变成两层。由一个root节点变成3个节点,一个root和两个子节点
  {
    unsigned char *root = malloc(_WIDTH + 1);//新生成的root节点
    memset(root, 0, _WIDTH + 1);
    root[0] = 0;
    to_big(root + 1, db->node_addrs[0]);//左子节点的地址
    strncpy(root + 1 + SIZEOF_LONG, key, _HASH);//key
    to_big(root + 1 + SIZEOF_LONG + _HASH, addr);//右子节点的地址
    fseeko(db->fp, 0, SEEK_END);
    addr = ftello(db->fp);
    fwrite(root, 1, _WIDTH, db->fp);//将新生成的root写入到文件末尾
    fseeko(db->fp, 0, SEEK_SET);
    fwrite(&addr, 1, SIZEOF_LONG, db->fp);//更新文件中记录的整个树的根节点,用新的root节点的地址覆盖原来记录的root的地址
  }
}

/**
 * 根据db的描述,将key和记录对应内容的地址addr构成的项插入到index层,同时附带右子树根节点地址,以及该项要插入的node是否为叶子节点。
 * 这里的难点是_insert()和node_split()函数相互调用。理解起来比较麻烦。
 */
void _insert(db *db, unsigned char *key, int index, uint64_t addr, uint64_t rptr, int isleaf)
{
  if (_HASH > strlen(key))
  {
    unsigned char *okey = key;
    key = malloc(_HASH + 1);
    memset(key, 0x61, _HASH + 1);
    strncpy(key, okey, strlen(okey));
  }
  unsigned char *node = db->path[index];//先读出插入位置所在的node
  int i = SIZEOF_LONG + 1;
  for (; i < _WIDTH; i += (_HASH + SIZEOF_LONG))//从node中找到key的具体位置
  {
    if (node[i] == 0)//该key没有找到;node[i]为null表示node中的最后一项,可以插入在该位置,更新key/value即可
    {
      i -= SIZEOF_LONG;
      to_big(node + i, addr);//把addr写到key前面的8个字节中
      i += SIZEOF_LONG;
      strncpy(node + i, key, _HASH);//把key写进去
      if (!isleaf)//不是叶子节点,说明是内部节点,把该项对应的右子树的根接单写入到node中。
      {
        i += _HASH;
        to_big(node + i, rptr);
      }
      break;
    }
    if (!strncmp(node + i, key, _HASH))//key在当前node中已经存在了
    {
      if (isleaf)//如果是叶子节点,则更新前面的value的地址,相当于覆盖以前的值。
      {
        i -= SIZEOF_LONG;
        to_big(node + i, addr);
      }
      break;
    }

    if (strncmp(node + i, key, _HASH) > 0)//node[i]>key,说明要往前插入;
    {
      unsigned char *nnode = malloc(_WIDTH + 1);//重新申请一个node
      memset(nnode, 0, _WIDTH + 1);
      i -= SIZEOF_LONG;
      int j;
      for (j = 0; j < i; ++j)
      {
        nnode[j] = node[j];
      }
      to_big(nnode + i, addr);
      i += SIZEOF_LONG;
      strncpy(nnode + i, key, _HASH);
      i += _HASH;
      if (!isleaf)//如果不是叶子节点,则需要把右子树的根地址插入
      {
        to_big(nnode + i, rptr);
        i += SIZEOF_LONG;
        j += SIZEOF_LONG;
      }

      for (; i < _WIDTH; ++i, ++j)
      {
        nnode[i] = node[j];
      }
      memcpy(db->path[index], nnode, _WIDTH);
      break;
    }
  }

  if (node[(_WIDTH - (_HASH + SIZEOF_LONG))] != 0)//前面插入一项完毕后,要判断当前node是否已经满了(最后一项的key是否为0),是否需要分裂
  {
    isleaf ? node_split(db, index, 1) : node_split(db, index, 0);
  }
  fseeko(db->fp, db->node_addrs[index], SEEK_SET);
  fwrite(db->path[index], 1, _WIDTH, db->fp);//如果只是更新(或者插入后没有引起分裂),则需要把更新的node写入到文件中进行覆盖。
}

/**
 * 查找key所在的位置(已经存在),或者应该所在的位置(待插入的位置),并将位置信息保存在r_index中,表示从根节点到该位置的路径。
 * @param key 待查找的key(使用字符串是一种通用表示,前面的to_big也是将整数标准化成可比较的字符串)
 * @param r_index [out] 从根节点到key的路径
 */
uint64_t
search(db *db, unsigned char *key, int *r_index)
{
  if (_HASH > strlen(key))
  { //如果key的长度不够,则重建key,后面字符a,这个我不赞同,如果仅仅用于纯小写英文字母组成的key判断,是没问题的,但是如果包含了大写字母或者数字以及其他字符,这个就会出错。
    unsigned char *okey = key;
    key = malloc(_HASH + 1);
    memset(key, 0x61, _HASH + 1);
    strncpy(key, okey, strlen(okey));
  }
  uint64_t r_addr; //指向root节点的指针

  int i = SIZEOF_LONG + 1; //node内部的游标,从0到_WIDTH,每次移动到key的开始位置或者存放左子树/右子树指针的地址。
  unsigned char isleaf;    //是否是叶子节点,通过node中的第一个字节是0或者1来判断。
  int index = 0;           //node内部的index计数,i是字节级别,index是单算key为第几个的,从0到_ORDER

  //从文件中读出root节点的地址,并定位到该地址,然后读出node节点并放到db->path[0]中,同时更新db->node_addrs[0]为root节点的地址
  fseeko(db->fp, 0, SEEK_SET); //定位到文件开头的位置,文件大小用off_t来表示,应该是64位
  fread(&r_addr, 1, SIZEOF_LONG, db->fp);
  fseeko(db->fp, r_addr, SEEK_SET);
  fread(db->path[index], 1, _WIDTH, db->fp);
  db->node_addrs[index] = r_addr;
//从root节点开始,往下遍历B+树查找key所在的位置,并将查找路径上的节点放到db->path[]中,并将找到这些节点的路径放到db->node_addrs[]中
search:
  isleaf = db->path[index][0]; //node中的第一个字节为0或者1表示是否为叶子节点
  //在当前node中遍历。因为key定长,指针也是定长,这俩组合可以视作一项,i这个游标每次移动一项
  for (; i < _WIDTH; i += (_HASH + SIZEOF_LONG))
  { 
    //如果当前node中的当前key1和待查找的key相同,则说明找到了
    if (!strncmp(db->path[index] + i, key, _HASH))
    {
      //如果是叶子节点,那就是找到了(因为B+树的内部节点的key可能和叶子节点的key一样)
      if (isleaf)
      {
        *r_index = index;//更新查找的深度作为返回值
        i -= SIZEOF_LONG;//移动i到指向该key对应内容的地址
        uint64_t cindex = from_big(db->path[index] + i);//获取存放内容的地址content_index
        fseeko(db->fp, cindex, SEEK_SET);
        //下面这一部分应该是将content读出去,至于多长,需要从某个地方获取。
        unsigned char check;
        fread(&check, 1, 1, db->fp);//读取1个字节。这里只是一个原型,用于验证读到的内容是否正确?
        if (check == 0)
        {
          return 1;
        }
        return 0;
      }
      
      //下面这个应该不会存在,index如果超过了最大深度,应该直接退出,而不用比较。如果index已经超过了最大深度,怎么还存在key和待查找的key相等的情况呢?
      if (index >= _DEPTH)
      {
        *r_index = 0;
        return -1;
      }
      //不是叶子节点,而是内部节点,则需要找其右子树啦。找到右子树的根节点,并将该node放到db->path[]中,将其地址放到db->node_addrs[]中
      i += _HASH;
      uint64_t addr = from_big(db->path[index] + i);
      fseeko(db->fp, addr, SEEK_SET);
      ++index;
      fread(db->path[index], 1, _WIDTH, db->fp);
      db->node_addrs[index] = addr;
      i = SIZEOF_LONG + 1;
      goto search;
    }
    
    //如果遍历的key1比待查找的key大,或者key1为0(本node只有这么多项啦,这是最后一项),都说明key不在本层,
    //可以往下遍历左子树或者结束(如果到了叶子节点这一层)。这个if用于控制跨层
    if (strncmp(db->path[index] + i, key, _HASH) > 0 ||
        db->path[index][i] == 0)
    {
      //如果当前node是叶子节点,则说明key在当前的B+树中不存在,当前B+树的深度为index。如果后面要插入节点,则插入到本node中。
      //前面查找的过程以及记录了路径和路径上的节点,至于真的插入的时候是否会引起分裂,则插入之后再看。
      if (isleaf)
      {
        *r_index = index;
        return 1;
      }
      //如果超过最大深度了,则说明整棵B+树满了,或者出错了。正常情况下,遍历过程中是不会超出最大深度的。
      if (index >= _DEPTH)
      {
        *r_index = 0;
        return -1;
      }
      //当前node不是叶子节点,则需要往下找左子树,而i当前指向了key,所以需要往前移动8字节,才能达到记录左子树地址的位置。
      i -= SIZEOF_LONG;
      uint64_t addr = from_big(db->path[index] + i);//地址为啥用大端表示?查找过程应该不会涉及地址的比较
      //读取左子树的根节点,并将它放到db->path[]中,并将左子树的根节点的地址放到db->node_addrs[]中。
      fseeko(db->fp, addr, SEEK_SET);
      ++index;
      fread(db->path[index], 1, _WIDTH, db->fp);
      db->node_addrs[index] = addr;

      i = SIZEOF_LONG + 1;//i定位到node中的第一个项的key的位置,开始在本node中查找
      goto search;
    }
  }
}

/**
 * 在B+树中插入一项key和value.并将相关信息保留在db中。
 * 我猜测文件的构成应该是开头8个字节表示root节点所在的位置,后面跟着一系列节点和记录(value)。
 * 每个记录包含1个字节的有效位,key_len和key构成的key字段,value_len和value构成的value字段.
 * B+树在使用的时候是从文件中读取出来(根据root节点及其左右子树地址),然后在内存中构建的,B+树本身也存到文件中了。
 */
void put(db *db, unsigned char *key, unsigned char *value)
{
  int index;
  uint64_t ret;
#ifdef LOCK
  if (db_lock(db) == -1)
  {
    perror("fcntl");
    return;
  }
  else
  {
#endif
    if ((ret = search(db, key, &index)) > 0)//key不存在,且找到了待插入的位置为合法位置
    {
      uint64_t k_len = strlen(key);
      uint64_t v_len = strlen(value);
      if (k_len + v_len > _MAX)
      {
        return;
      }
      uint64_t n_len = k_len + v_len + SIZEOF_LONG + SIZEOF_LONG + 1;//用于存储value的空间,格式为"0/1 key_len key value_len value".

      unsigned char *nnode = malloc(n_len + 1);//new node
      unsigned char *ptr = nnode;
      memset(nnode, 0, n_len + 1);
      nnode[0] = 1;//先填充有效位
      to_big(ptr + 1, k_len);//先填充key_len(大端表示)
      strncpy(ptr + SIZEOF_LONG + 1, key, k_len);//再填充key
      to_big(ptr + SIZEOF_LONG + k_len + 1, v_len);//填充value_len
      strncpy(ptr + SIZEOF_LONG + k_len + SIZEOF_LONG + 1, value, v_len);//填充value
      fseeko(db->fp, 0, SEEK_END);//定位到文件末尾处
      uint64_t addr = ftello(db->fp);//获取当前位置
      fwrite(nnode, 1, n_len, db->fp);//将前面构建的value结构写入到文件
      _insert(db, key, index, addr, 0, 1);//key和addr构成的<k,v>插入到B+树中
    }
#ifdef LOCK
    if (db_unlock(db) == -1)
    {
      perror("fcntl");
      return;
    }
  }
#endif
  fflush(db->fp);
}

unsigned char *
get(db *db, unsigned char *key)
{
  int index;
  if (_HASH > strlen(key))//先将key补齐到_HASH大小,后面填充'a',在插入的时候估计也是把key先补齐到_HASH大小了
  {
    unsigned char *okey = key;
    key = malloc(_HASH + 1);
    memset(key, 0x61, _HASH + 1);
    strncpy(key, okey, strlen(okey));
  }

  if (!search(db, key, &index))//如果找到了
  {
    int i = SIZEOF_LONG + 1;
    for (; i < _WIDTH; i += (SIZEOF_LONG + _HASH))//在db->path[index]这个node内遍历查找key对应的项item
    {
      if (!strncmp(db->path[index] + i, key, _HASH))//找到了
      {
        i -= SIZEOF_LONG;
        uint64_t addr = from_big(db->path[index] + i);//找到key对应的addr,该addr记录了对应的内容在文件中的位置。
        fseeko(db->fp, addr, SEEK_SET);
        unsigned char exists;
        unsigned char k_len[SIZEOF_LONG];
        unsigned char v_len[SIZEOF_LONG];
        fread(&exists, 1, 1, db->fp);
        if (!exists)//该记录无效
        {
          return NULL;
        }
        //该记录有效,读取key和value
        fread(k_len, 1, SIZEOF_LONG, db->fp);
        uint64_t k_lenb = from_big(k_len);
        unsigned char *k = malloc(k_lenb);
        memset(k, 0, k_lenb);
        fread(k, 1, k_lenb, db->fp);
        fread(v_len, 1, SIZEOF_LONG, db->fp);
        uint64_t v_lenb = from_big(v_len);
        unsigned char *v = malloc(v_lenb);
        memset(v, 0, v_lenb);
        fread(v, 1, v_lenb, db->fp);
        return v;//返回value
      }
    }
    return NULL;
  }
  return NULL;
}

/**
*delete并不真实删除数据,只是把记录的第一个字节从1变成了0,表示数据无效。
*/
void delete (db *db, unsigned char *key)
{
  int index;
  if (_HASH > strlen(key))
  {
    unsigned char *okey = key;
    key = malloc(_HASH + 1);
    memset(key, 0x61, _HASH + 1);
    strncpy(key, okey, strlen(okey));
  }
  if (!search(db, key, &index))
  {
    int i = SIZEOF_LONG + 1;
    for (; i < _WIDTH; i += (SIZEOF_LONG + _HASH))
    {
      if (!strncmp(db->path[index] + i, key, _HASH))
      {
        i -= SIZEOF_LONG;
        unsigned char del = 0;
        uint64_t addr = from_big(db->path[index] + i);
        fseeko(db->fp, addr, SEEK_SET);
        fwrite(&del, 1, 1, db->fp);
      }
    }
  }
}

void db_init(db *db, const char *name)
{
  uint64_t addr;
  unsigned char *zero = malloc(_WIDTH);
  memset(zero, 0, _WIDTH);
  zero[0] = 1;
  db->fp = fopen(name, "rb+"); //尝试读写的方式打开一个确定存在的文件,如果文件不存在则打开失败,返回null。
  if (!db->fp)
  { //如果能打开,说明文件已经存在(但是无法判断是否是存储B+树的内容)
    db->fp = fopen(name, "wb+");
    addr = SIZEOF_LONG; //8
    fseeko(db->fp, 0, SEEK_SET);
    fwrite(&addr, SIZEOF_LONG, 1, db->fp); //写文件头的8个字节,内容是08 00 00 00 00 00 00 00 (小端)
    fwrite(zero, 1, _WIDTH, db->fp);       //写文件的第9个字节至8+_WIDTH字节,共计_WIDTH字节,写一个空的叶子node,(node中的第一个字节为1,表示叶子节点)
  }
}

void db_close(db *db)
{
  fclose(db->fp);
}

/*** function for testing,可以删除***/

char *random_str()
{
  int i;
  char *alphabet = "abcdefghijklmnopqrstuvwxyz";
  char *string = malloc(33);//上面的_HASH为33是测试用的,和这里一致
  for (i = 0; i < 32; i++)
  {
    string[i] = alphabet[rand() % 26];
  }
  string[i] = 0;
  return string;
}

/***************************/

int main(void)
{
  db new;
  db_init(&new, "test");
  /*put(&new, "hello", "world");
  put(&new, "123", "4567890");
  put(&new, "abd", "defghijklmn");
  put(&new, "jsc", "04110");
 
 char *value = get(&new, "hello");
  puts(value);
  */
  db_close(&new);
  return 0;
}

这里给出两个运行截图:

(1)只进行文件初始化

文件大小4076字节。按照上面计算出来的结果,_WIDTH=4068,文件开头的8个字节表示root的地址,共计4076字节。文件内容的前8个字节记录了root节点地址(08 00 00 00 00 00 00 00),和1个root节点(第一个字节为01表示是叶子节点)。

(2)插入几条数据

文件大小为4186字节,前面4076字节和(1)中的解释一样,包括8个字节表示的root地址,和1个root节点(内容发生了更新,也就是增加了几个key和左右指针),在root节点之后,是数据部分,包含了几条数据,这些数据的地址和root节点中的左右指针一致。

 

哈夫曼树的构造是一种用于数据压缩的算法。其基本思想是将出现频率较高的字符进行编码,使得编码后的数据长度更短,从而实现数据压缩的目的。 以下是哈夫曼树的构造代码注释: // 定义哈夫曼树结点 struct HuffmanNode { int weight; // 结点权重 int parent; // 父结点在数组中的下标 int leftChild, rightChild; // 左右孩子在数组中的下标 }; // 定义比较哈夫曼树结点权重的函数 bool cmp(HuffmanNode a, HuffmanNode b) { return a.weight > b.weight; } // 构造哈夫曼树 void buildHuffmanTree(HuffmanNode huffmanTree[], int n) { int i, j; // 初始化哈夫曼树结点 for (i = 0; i < 2 * n - 1; i++) { huffmanTree[i].parent = -1; huffmanTree[i].leftChild = -1; huffmanTree[i].rightChild = -1; } // 输入每个结点的权重 for (i = 0; i < n; i++) { cin >> huffmanTree[i].weight; } // 构造哈夫曼树 for (i = n; i < 2 * n - 1; i++) { // 在剩余结点中找到权重最小的两个结点 int min1, min2; min1 = min2 = INT_MAX; for (j = 0; j < i; j++) { if (huffmanTree[j].parent == -1 && huffmanTree[j].weight < min1) { min2 = min1; min1 = huffmanTree[j].weight; } else if (huffmanTree[j].parent == -1 && huffmanTree[j].weight < min2) { min2 = huffmanTree[j].weight; } } // 将这两个结点合并为一个新结点,并添加到哈夫曼树中 huffmanTree[i].weight = min1 + min2; for (j = 0; j < i; j++) { if (huffmanTree[j].weight == min1 && huffmanTree[j].parent == -1) { huffmanTree[j].parent = i; huffmanTree[i].leftChild = j; } else if (huffmanTree[j].weight == min2 && huffmanTree[j].parent == -1) { huffmanTree[j].parent = i; huffmanTree[i].rightChild = j; } } } } 笑话:为什么大象会跑得很快?因为小老鼠追得紧啊!
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值