一、树的一些基本概念
节点的度:一个节点含有的子树的个数称为该节点的度;
叶节点或终端节点:度为0的节点称为叶节点;
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;
如上图:B是A的孩子节点兄弟节点:具有相同父节点的节点互称为兄弟节点;
树的度:一棵树中,最大的节点的度称为树的度;
树的高度或深度:树中节点的最大层次;
森林:由m(m>0)棵互不相交的树的集合称为森林。
树是递归定义的。任何一棵树都包含根和n(n>=0)棵子树两个部分。
子树互不相交(如果相交就是另一种数据结构(图))
除了根节点以外,每个节点有且仅有一个父节点。
1.1 树的节点设计
如何设计树的节点可以表示出树的数据结构呢?
这样设计:
struct TreeNode
{
int val;
struct TreeNode* leftchild;
struct TreeNode* rightBrother;
};
每个节点中有两个指针,一个指向这个节点的第一个(最左边的)子节点,另一个指向这个节点的右边的一个亲兄弟节点。这样从根节点出发就可以找到整棵树的所有节点,非常巧妙。
1.2 二叉树的概念
二叉树是指每个节点最多有两个子节点的树。
满二叉树指的是每个层的节点数都达到最大值的二叉树。h层的满二叉树共有(2^h-1)个节点。
如果一棵满二叉树有N个节点,那么它的层数是log2(N+1)层。
完全二叉树指的是从上到下、从左到右依次填满节点,中间没有空位的二叉树。也就是说,完全二叉树除了最后一层以外的所有层都是满的,并且最后一层的节点是从左到右连续的(不能有空的位置)。
完全二叉树在内存中是连续存储的。这样存储比较抽象,但是它的好处是可以通过下标来计算父子关系:假设父节点在数组中的下标是i,那么左孩子在数组中的下标是2*i+1,右孩子在数组中的下标是2*i+2。如果孩子节点在数组中的下标是j,那么父节点在数组中的下标就是(j-1)/2。

然而,如果不是完全二叉树,就不适合用数组来存储了。
二、堆
2.1 堆的概念
计算机中有两组堆和栈的概念,注意辨析:堆和栈可以是内存中两个不同的区域划分,也可以是两种数据结构。
堆是完全二叉树。分为两种。大堆中每个父节点都大于子节点。小堆中每个父节点都小于子父节点。
2.2 代码实现
2.2.1 初始化与销毁
void HPInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
void HPDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
2.2.2 插入数据
插入数据需要把数据按照顺序放在堆的最后,然后向上调整。以大堆为例,如果当前这个数据比它的父节点的数据大,就让两者交换位置,直到它不再比父节点大。所以,我们需要写一个函数用于向上调整;然后还需要一个用于交换数组中两数据位置的辅助函数。
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;
}
}
}
void HPPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
php->capacity = newcapacity;
HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc failed:");
return;
}
php->a = tmp;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
2.2.3 删除数据
删除数据是指删除根节点上那个数据,因为如果是删最后一个数据,只需要size--,没什么写的必要。删除根节点数据,我们先让它与堆里的最后一个数据互换位置,再size--,再把根节点处的数据向下调整到正确的位置。因为每个父节点有两个子节点,所以需要比较一下两个子节点的大小再决定和哪个交换。
AdjustDown(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while(child < n)//child>=n说明孩子不存在了
{
if (child + 1 < n && 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 HPPop(HP* php)
{
assert(php);
assert(php->size > 0);
Swap(&php->a[php->size - 1], &php->a[0]);
php->size--;
AdjustDown(php->a,php->size,0);
}
2.2.4 取顶与判空
HPDataType HPTop(HP* php)
{
assert(php && php->size);
return php->a[0];
}
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
三、堆排序
我们可以利用堆来进行效率很高的排序。
比如有一个数组。如果要找到前k个最大的数据,我们只需把它们一个一个插入到一个大堆里,然后取根节点数据(此时根节点的数据就是最大的那个),然后删除它,然后再取根节点数据(此时根节点数据就是第二大的数据)......取k次,就得到前k个最大的数据了。每次取出后按顺序放到数组里,就完成了数组的排序。
void TestHeap1()
{
int a[] = { 4,2,8,1,5,6,9,7,3,23,444,65,80,3533,66,245};
HP hp;
HPInit(&hp);
for (size_t i = 0; i < sizeof(a)/sizeof(int); i++)
{
HPPush(&hp, a[i]);
}
//找出最大的前k个:
int k = 0;
scanf("%d", &k);
int i = 0;
while (k--)
{
printf("%d ", HPTop(&hp));
a[i++] = HPTop(&hp);//数组排序
HPPop(&hp);
}
printf("\n");
HeapDestroy(&hp);
}
那如果要在数组内部完成排序,而不是先插入堆中再复制出来,该怎么办呢?此时就需要建堆,然后借鉴在堆中删除数据的思路排序,只不过这次不是删除,而是“尾删除”——把数据放到数组的最后面。
void HeapSort(int* a, int n)
{
//建堆
for (int i = 1; i < n; i++)
{
AdjustUp(a, i);
}
int end = n - 1;
while (end>0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
void TestHeap2()
{
int a[] = { 4,2,8,1,5,6,9,7,3 };
HeapSort(a, sizeof(a)/sizeof(int));
}
int main()
{
TestHeap2();
return 0;
}
这种排序算法的效率是很高的,远高于之前讲过的冒泡排序(冒泡排序没有实用意义,只有教学意义)。
刚才我们用向上调整的方法建堆,但其实我们还可以用向下调整的方法来建堆。
//向下建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
容易推得向下调整建堆的时间复杂度是O(N),向上调整建堆的时间复杂度是O(N+logN)。
向下调整建堆的效率更优,因为向上调整建堆是节点数多的层调整次数更多,而向下调整建堆是节点数少的层调整次数更多。
四、Top-k问题
刚才讲过的找最大的k个数据的方法,效率很高,时间复杂度是O(N*logN),但是问题在于要建堆必须在内存上开出一个数组,如果数据很多,内存就不够用。
更好的方法是这样的:
用前k个数据建一个小堆,让后面的数据依次与堆顶数据比较,如果比堆顶的数据大,就覆盖掉堆顶的数据,然后向下调整。这样,最后小堆中剩下的k个数据,就是最大的k个。
我们可以造一些数据文件来试一试这个算法。
void CreateNData()
{
int n = 100000;
srand(time(0));
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL)
{
perror("fopen error");
return;
}
for (int i = 0; i < n; ++i)
{
int x = (rand()+i) % 10000000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
数据造好了。我们来实现一下这个算法。
void TestHeap3()
{
int k;
printf("请输入k:");
scanf("%d", &k);
int* kminheap = malloc(sizeof(int) * k);
if (kminheap == NULL)
{
perror("malloc failed");
return;
}
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen error");
return;
}
for (int i = 0; i < k; i++)
{
fscanf(fout,"%d", &kminheap[i]);
}
//建一个k个数的小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(kminheap, k, i);
}
//读取剩下的N-K个数
int x = 0;
while (fscanf(fout,"%d", &x) > 0)
{
if (x > kminheap[0])
{
kminheap[0] = x;
AdjustDown(kminheap, k, 0);
}
}
printf("最大的前%d个数是:", k);
for (int i = 0; i < k; i++)
{
printf("%d ", kminheap[i]);
}
printf("\n");
}
我们已经通过%10000000将造出的数据控制在了10000000以内,接下来我们就可以手动修改数据来看看算法好不好用。

成功挑出了手动修改过的10个数据。说明算法没有问题。
堆的知识点到这里就差不多结束了。
END
2113



被折叠的 条评论
为什么被折叠?



