数据结构---堆(线性的完全二叉树)

目录

一、树的概念及结构

1.树的概念

1.2树的相关概念

1.3树的表示

1.4树在实际中的运用

二、二叉树的概念及结构

2.1二叉树的概念

2.2现实中的二叉树

2.3特殊的二叉树

2.4二叉树的性质

2.5二叉树的存储结构

三、二叉树的顺序结构及实现

3.1二叉树的顺序结构

3.2堆的概念及结构

3.3堆的实现

3.3.1堆的向下调整算法

3.3.2堆的创建

3.3.3堆的插入

3.3.4堆顶元素的删除

3.3.5堆的实现代码

3.3.6堆的实际运用

(1)堆排序

(2)TopK问题       


一、树的概念及结构

1.树的概念

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

(1)有一个特殊的节点,称为根节点,根节点没有前驱节点

(2)除了根节点之外,其余节点被分为M个互不相交的集合T1,T2,T3.....Tn其中每一个集合Ti又是一棵结构与树一样的子树。每一棵子树的根节点有且只有一个前驱节点,其后可以有0个或多个后继节点,因此树是递归定义的。

像图二就是一颗树!

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

1.2树的相关概念

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

叶子节点:度为0的节点称为叶子节点,也就是没有子树的节点,如上图的H,I等

分支节点:度不为0的节点称为分支节点,如上图的D,E等

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

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

兄弟节点:具有相同父节点的节点互为兄弟节点,如上图的P,Q等

树的度:一棵树中,最大节点的度被称为该树的度,如上图树的度就是A节点的度,也就是6

节点的层次:从根节点开始,为第一个层,依次往下递增

树的高度或者深度:树中节点的最大层次,如上图树的高度就是4

堂兄弟节点:父节点在同一层的节点互为堂兄弟节点,如上图的H,I等

节点的祖先:从根到该节点路径上的所有节点都是该节点的祖先,如E是J的祖先,E也是P的祖先

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

森林:由m棵互不相交的树构成的集合称为森林

1.3树的表示

        树的结构相对于线性表就复杂了许多,要存储数据也会变得比较麻烦,既要存储有效的数据,还需要存储节点间的关系,实际上树有很多种表示形式:父亲表示法,孩子表示法,孩子父亲表示法,以及孩子兄弟表示法。而这些结构中的王者是:左孩子右兄弟表示法。

我们来看看这种表示法的结构

typedef int Datatype
typedef struct TreeNode
{
    struct TreeNode* firstchild;    //指向第一个孩子
    struct TreeNode* nextbrother;   //指向下一个兄弟
    Datatype data;                  //存放节点数据
}TreeNode;

        他的优点就是不管你一个节点下有多少个孩子或者兄弟,我都只存储第一个孩子和下一个兄弟,如果我要访问某一个孩子或兄弟只需要通过指着一步步去遍历即可,这种结构出现让我们再也不会被未知的节点个数而头痛了。

1.4树在实际中的运用

        树在实际中运用最多的地方就是目录树,无论是Windows还是Linux,他们的文件夹(目录)都是用树的形式来存储的,就比如我们在访问D盘的时候,其实就是因为D盘这个节点存储了第一个孩子和下一个兄弟,当我们双击D盘的时候就是通过第一个孩子把它所有的孩子都列举显示到屏幕上。

二、二叉树的概念及结构

2.1二叉树的概念

一棵二叉树是节点的一个有限集合,该集合只有两种情况:

(1)为空

(2)由一个根节点加上两棵别称为左子树和右子树的二叉树组成

从上图可以看出来:

(1)二叉树不存在度大于2的节点

(2)二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

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

2.2现实中的二叉树

2.3特殊的二叉树

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

(2)完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。简而言之,完全二叉树就是满二叉树后一行多了几个连续的节点,即只有最后一行不满。

2.4二叉树的性质

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

(2)若规定一棵树的根节点层次为1,则深度为h的二叉树的最大节点数是2^{h}-1

(3)对于任何一棵二叉树,如果度为0的叶子节点有n1个,度为2的分支节点个数为n2,

则n1=n2+1

(4)若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=log 2(n+1) .

(ps: log 2(n+1)是log以2为底,n+1为对数)

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

        1. 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
        2. 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
        3. 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子

2.5二叉树的存储结构

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

(1)顺序结构:

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

(2)链式结构:

        二叉树的链式结构是指,用链表来表示一棵二叉树,即用指针来表示节点之间的逻辑关系。通常使用的方法是,每个节点由三个区域组成:数据域和左右指针域,左右指针来指向左右孩子所在的节点,链式结构一般还有三叉链,我们以后在红黑树等会学习到三叉链

typedef int Datatype;
struct BinaryTreeNode
{
    struct BinaryTreeNode* leftchild;
    struct BinaryTreeNode* rightchild;
    Datatype data;
};
typedef int Datatype
struct BinaryTreeNode
{
    struct BinaryTreeNode* parent;
    struct BinaryTreeNode* leftchild;
    struct BinaryTreeNode* rightchild;
    Datatype data;
};

三、二叉树的顺序结构及实现

3.1二叉树的顺序结构

        普通的二叉树是不适合使用数组来实现的,因为可能会存在大量的空间浪费。而完全二叉树就不会存在这个问题,因为他的定义就是连续的没有空间间断的,所以他非常适合使用数组来存储。显示中,我们把堆(一种特殊的完全二叉树)使用顺序结构(数组)来存储,值得注意的是,这里说的堆和操作系统中的堆不是一码事,一个数一种数据结构,一个是操作系统管理内存的一块区域分段。

3.2堆的概念及结构

       

                    

3.3堆的实现

3.3.1堆的向下调整算法

        对于所有的数组,我们都可以看做是一个完全二叉树。我们通过根节点开始向下调整的算法可以把他调整成一个堆。向下调整算法的前提:左右子树必须是一个堆

int array[] = {27,15,19,18,28,34,65,49,25,37};

3.3.2堆的创建

        下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。

int a[] = {1,5,3,8,7,6};

3.3.3堆的插入

        先插入一个10到数组中,然后通过向上调整算法,直到满足堆的条件

3.3.4堆顶元素的删除

        因为堆中的父子关系已经确定了,如果和往常一样直接挪动覆盖的话,父子关系就会完全被破坏,从父子变成了兄弟等(因为堆只有父子间才满足大小关系,兄弟间无所谓),一旦这样再次建堆的时间复杂度会变得很高,而且挪动覆盖本身也是一个效率极低的事情。所以堆的删除数据是先把堆顶的元素和最后一个交换,这样就不会破坏原有的父子关系,然后再通过向下调整算法,把被稍稍破坏的堆重建起来,这样的时间复杂度相当理想!

3.3.5堆的实现代码

typedef int Datatype;
typedef struct Heap
{
	Datatype* a;
	int size;
	int capacity;
}Heap;


void Swap(Datatype* p1,Datatype* p2)
{
	Datatype tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void AdjustUp(Datatype* a,int child)
{
	int parent = (child - 1) / 2;
	//当child没有走到堆顶的时候一直走
	while (child>0)
	{
		//如果孩子比父亲小,就交换(建小堆)
		if (a[child]<a[parent])
		{
			Swap(&a[child],&a[parent]);
			//迭代
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

void AdjustDown(Datatype* a,int size,int parent)
{
	int child = parent * 2 + 1;
	//当孩子没有越界的时候一直调整
	while (child<size)
	{
		//找到孩子中小的那一个(因为在建小堆)
		//child+1是右孩子,如果右孩子越界了说明不存在,就不比较了
		if (child+1<size&&a[child+1]<a[child])
		{
			child++;
		}
		//判断是否和父亲交换
		if (a[child]<a[parent])
		{
			Swap(&a[child],&a[parent]);
			//迭代
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}


void HeapInit(Heap* heap)
{
	assert(heap);
	heap->a = NULL;
	heap->capacity = 0;
	heap->size = 0;
}

void HeapDestroy(Heap* heap)
{
	assert(heap);
	free(heap->a);
	heap->a = NULL;
	heap->size = 0;
	heap->capacity = 0;
}

bool HeapEmpty(Heap* heap)
{
	assert(heap);
	return heap->size == 0;
}

int HeapSize(Heap* heap)
{
	assert(heap);
	return heap->size;
}

Datatype HeapTop(Heap* heap)
{
	assert(heap);
	assert(!HeapEmpty(heap));
	return heap->a[0];
}

void HeapPush(Heap* heap,Datatype x)
{
	assert(heap);
	//判断是否需要扩容
	if (heap->capacity==heap->size)
	{
		int newcapacity = heap->capacity == 0 ? 4 : heap->capacity * 2;
		Datatype* tmp = (Datatype*)realloc(heap->a,sizeof(Datatype)*newcapacity);
		if (tmp==NULL)
		{
			perror("realloc");
			return;
		}
		heap->a = tmp;
		heap->capacity = newcapacity;
	}
	//插入数据
	heap->a[heap->size] = x;
	heap->size++;
	//向上调整
	AdjustUp(heap->a,heap->size-1);
}

//堆的删除为了不破坏父子关系,都是把堆顶的元素拿到最后去,然后把剩下的n-1个元素重新建堆
void HeapPop(Heap* heap)
{
	assert(heap);
	assert(!HeapEmpty(heap));
	//交换堆顶和最后一个元素
	Swap(&heap->a[0],&heap->a[heap->size-1]);
	heap->size--;
	//向下调整重建堆
	AdjustDown(heap->a,heap->size,0);

}

3.3.6堆的实际运用

(1)堆排序

        堆排序是利用堆的思想来进行排序,他的效率相比于我们之前所学的冒泡排序要高许多,时间复杂度是N*logN,关于这个我们会在后面进行证明。

        堆排序的步骤:

1.建堆

        升序建大堆,降序建小堆(想一想为什么)

2.利用堆的删除的思想来进行排序,

建堆和堆的删除都用到了向下调整,因此只要掌握了向下调整就能够建堆!

我们当然可以用之前的堆的这种数据结构,但是这样做有两大问题:

1.空间复杂度高(又创建了一个堆)

2.还需要把堆里面的数据拷贝到原数组中,这也是一个很麻烦的事情

所以我们在这里就是直接在原数组上进行向下调整来把原数组建堆,然后HeapPop移动堆顶的数据到末尾去,再把它重新向下调整建堆,周而复始直到size减为0堆排序就完成了

void HeapSort(int* arr,int size)
{
	//从第一个非叶子节点开始向下调整,因为叶子节点本身就可以看做一个堆
	//i>=0是因为根节点也需要向下调整一次,
	//i--是因为从第一个父亲开始往前面的每一个节点都是父节点

	//建堆
	for (int i=(size-1-1)/2;i>=0;i--)
	{
		AdjustDown(arr,size,i);
	}

	int end = size - 1;
	while (end>0)
	{
		//把第一个数据拿到最后去
		Swap(&arr[0], &arr[end]);
		//拿出堆顶的数据后,原堆就被破坏了,需要重新建堆
		//这里传end是因为end既是最后一个元素的下标也是前面元素的个数
		AdjustDown(arr,end,0);
		//每一次end都往前走一步,因为已经把最大的数据放到最后去了
		end--;
	}
}
(2)TopK问题       

         TopK问题:求数据中最大/最小的前K个数,一般数据量都非常大,比如专业前10,世界500强,全球富豪榜,游戏前100名玩家等。

        对于TopK问题最简单最直接的方法就是排序,但是如果数据量非常大,排序就不太可取了,一是因为排序需要对每一个数都排序,这样时间成本会很高,二是当数据量极大的时候,不可能被一次性加载到内存中,(比如一次有100G的数据)而我们知道排序是一个程序,他需要加载到内存中才能运行,这样明显不行了。

        那么最好的办法就是用堆来解决了!基本思路如下:

1.用前K个数据建堆,如果是前K个最大的数据,则建小堆(前K个最小的数据则是建大堆)

2.用剩余的N-K个元素依次与堆顶的数据进行比较,如果比堆顶的数据大就和堆顶的数据替换,并且向下调整

3.将剩余的N-K个元素比较完后堆中剩下的就是前K个最大的数据了

先用我们前面所学的文件操作来创建大量的随机数

void CreatDatas()
{
	//创造很大量的数据
	int n = 10000;
	srand(time(0));
	const char* file = "datas.txt";
	FILE* fin = fopen(file,"w");
	if (fin==NULL)
	{
		perror(file);
		return;
	}
	for (size_t i=0;i<n;i++)
	{
		int x = rand() % 1000000;
		fprintf(fin,"%d\n",x);
	}
	fclose(fin);
}

这是创建的数据文件内容,我们把其中的某5个数改掉了,看这种算法能不能找到最大的K个数

TopK的算法

void TopK(int K)
{
	const char* file = "datas.txt";
	FILE* fout = fopen(file,"r");
	if (fout==NULL)
	{
		perror("fopen");
		return;
	}
	int* kHeap = (int*)malloc(sizeof(int)*K);
	if (kHeap==NULL)
	{
		perror("malloc");
		return;
	}
	//提取前K个数据
	for (int i=0;i<K;i++)
	{
		fscanf(fout,"%d",&kHeap[i]);
	}
	//把前K个数据建堆
	for (int i=(K-1-1)/2;i>=0;i--)
	{
		AdjustDown(kHeap,K,i);
	}
	//找到最大的K个数据
	int val = 0;
	while (!feof(fout))//文件结束返回非0,没有结束返回0
	{
		fscanf(fout,"%d",&val);
		if (val>kHeap[0])
		{
			kHeap[0] = val;
			AdjustDown(kHeap,K,0);
		}
	}
	//打印堆
	for (int i=0;i<K;i++)
	{
		printf("%d ",kHeap[i]);
	}
	printf("\n");
}

可以看到他非常迅速的完成了查找最大的前K个数,完美的解决了内存不够的问题,同时效率还极高!

这就是TopK问题的解决办法了,希望你能有所收获哦!

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值