深挖数据结构:树和二叉树(上)

目录

[1. 树](#1. 树)

[1.1 概念](#1.1 概念)

[1.2 基本术语](#1.2 基本术语)

[1.2.1 结点间的关系描述](#1.2.1 结点间的关系描述)

[1.2.2 结点和树之间的属性描述](#1.2.2 结点和树之间的属性描述)

[1.3 树的基本性质](#1.3 树的基本性质)

[2. 二叉树](#2. 二叉树)

[2.1 概念](#2.1 概念)

[2.2 特殊的二叉树](#2.2 特殊的二叉树)

[2.2.1 满二叉树](#2.2.1 满二叉树)

[2.2.2 完全二叉树](#2.2.2 完全二叉树)

[2.2.3 搜索二叉树](#2.2.3 搜索二叉树)

[2.3 二叉树性质](#2.3 二叉树性质)

[2.4 二叉树的存储](#2.4 二叉树的存储)

[2.4.1 顺序存储 - 堆](#2.4.1 顺序存储 - 堆)

[2.4.1.1 概念](#2.4.1.1 概念)

[2.4.1.2 堆代码](#2.4.1.2 堆代码)

堆的结构

堆的初始化

堆的判空

堆的销毁

向上调整建堆

堆的插入

向下调整建堆

堆的删除

[2.4.1.3 建堆](#2.4.1.3 建堆)

方法一:向上调整算法

方法二:向下调整算法

时间复杂度对比

测试代码

[2.4.2 堆经典题目](#2.4.2 堆经典题目)

堆排序

top-k问题


1. 树

1.1 概念

定义:树是n (n >= 0) 个结点的有限集。当 n = 0 时,称为空树。当 n > 0 时,称为非空树。

任意一颗非空树应该满足以下要求:

(1) 有且仅有一个叫做"根"的特定结点。

(2) 当 n > 1 时,其余结点可分为m(m>0)个互不相交的有限集,其中每个集合本身又是一棵树,称为"根的子树"。
注意:

**(1)**树的根节点没有前驱,除根结点以外的结点有且只有一个前驱结点,树中所有结点可以有多个后继结点。

**(2)**m叉树是指每个结点的度最大为 m,最少的度可以是 0,m叉树中不一定存在有m度的结点。

简图:

1.2 基本术语

1.2.1 结点间的关系描述

**(1)**祖先:从根一路走到这个结点,路上经过的所有结点,都是它的祖先。例子:根 A → C → G,A、C 都是 G 的祖先。

**(2)**子孙:一个结点下面,它的子树里面所有结点,全是它的子孙。例子:A 下面所有点都是 A 的子孙;C 下面只有 G,所以 C 的子孙只有 G。

**(3)**双亲:直接连着它、在它上面一层的那个结点。例子:B 往下直接连 E、F,B 是 E、F 的双亲。

**(4)**孩子:它直接指向下一层的结点。例子:E、F 是 B 的孩子。

**(5)**兄弟:同一个双亲生出来的结点,互相叫兄弟。例子:H 和 I 的双亲是同一个点,H、I 互为兄弟。

**(6)**堂兄弟:各自双亲在同一层,但是双亲不是同一个,这两个结点就是堂兄弟。例子:G 的双亲 C,H 的双亲 B;B 和 C 在同一层,所以 G 和 H 是堂兄弟。


1.2.2 结点和树之间的属性描述

**(1)**度:树中结点的孩子个数称为该结点的"度",树中结点的最大度数称为"树的度"。如:下图中,A结点的度是3,B结点的度是2,树的度是3。

**(2)**叶子结点/终端结点:度为0的结点称为"叶子结点"或"终端结点"。如:下图中的绿色结点。

**(3)**分支结点/非终端结点:度不为0的结点称为"分支结点"或"非终端结点"。

**(4)**结点的层次:结点的层次从树根结点开始定义,根结点A为第一层,分支结点B、C、D为第二层,以此类推。

**(5)**树的高度/深度:树种结点的最大层数称为"树的高度/深度"。如:下图中树的高度为4。(有的地方根从第 0 层开始算,做题留意)

**(6)**有序树/无序树:有序树中结点的子树有左右顺序,不能随便交换位置;最左边叫"第一个孩子",最右边叫"最后一个孩子"。

**(7)**路径:两个结点之间,沿着上下连线经过的一串结点。

路径长度:这条路径上结点的个数。(有的地方定义为边是数量,做题留意)

**(8)**边:双亲结点和它孩子结点之间的连线,叫做"边"。


1.3 树的基本性质

性质一:树中的结点数 = 所有结点的度数之和 + 1

理解:树中"所有结点度数之和"为除根节点外的所有结点数量之和;"+1"加的就是根结点。

性质二:m叉树的第 i 层上至多有 个结点。(i >= 0)

理解:要让第 i 层结点数量达到最大值上一层每一个结点都生出全部 m 个孩子。

性质三:高度为h的m叉树至多有个结点。

理解:每一个分支都长满m个孩子,每层全部填满,所有叶子都在第h层。由等比数列求和算出。


2. 二叉树

2.1 概念

定义:每个结点最多 2 棵子树(结点的度 ≤ 2,不存在度大于 2 的结点;树的度不一定等于2),是一个递归定义的数据结构。

👉重点:是有序树!子树分左、右,不能随便互换,分别叫左子树、右子树。

2.2 特殊的二叉树

2.2.1 满二叉树

定义:高度h,每一层结点全部填满、达到最大值,就是满二叉树。

特点:

1、叶子结点全部集中在最后一层。

**2、**所有分支结点的度一定等于 2(每个非叶子结点都同时有左、右孩子)。

**3、**同高度下,满二叉树是结点数量最多的二叉树。

**4、**结点总数:

2.2.2 完全二叉树

定义:高度h:前 h - 1 层全部填满;第 h 层可以不满,但结点必须从左往右连续排列,不能中间空缺。

关系:满二叉树一定是完全二叉树;完全二叉树不一定是满二叉树。

特点:

1、叶子结点只可能在最后两层。

2、度为 1 的结点最多只能有 1 个。

**3、**高度h的完全二叉树,结点数量范围:。

2.2.3 搜索二叉树

定义:二叉搜索树也叫二叉排序树,它要么是一棵空树,要么是满足特定规则的二叉树;最典型的特征是:对其做中序遍历,得到的结果是有序序列。

性质:

1、左子树约束 :若左子树不为空,左子树上所有结点的值都小于 根结点的值。(实践中存在变形规则:左子树所有结点的值可小于等于根结点的值)

2、右子树约束 :若右子树不为空,右子树上所有结点的值都大于 根结点的值。(实践中存在变形规则:右子树所有结点的值可大于等于根结点的值。)

3、递归性质:它的左子树、右子树本身,也分别是一棵二叉搜索树。

二叉搜索树是二叉树最核心的应用方向之一,是 "查找" 相关知识模块的重点数据结构,会在查找章节做系统的深入讲解。


2.3 二叉树性质

性质一:二叉树的第 i 层上至多有个结点(i >= 1)

性质二:高度为h的二叉树至多有个结点。

性质三:对任意二叉树,若度为 0 的叶子结点数为,度为 2 的结点数为 ,

则恒满足:

理解:设总结点数为m,度为1的结点数为,则,又因 总结点数=所有结点度之和+1,则 ,联立求得

性质四:具有 n 个结点的完全二叉树,高度为:

理解:高度为 h 的完全二叉树,结点数满足:,可以缩放为:;对不等式取以 2 为底的对数,得;由于高度 h 是整数,因此,整理得。


2.4 二叉树的存储

2.4.1 顺序存储 - 堆

2.4.1.1 概念

定义:堆是顺序存储结构,逻辑上是一颗完全二叉树。

堆又分为:小根堆、大根堆。

小根堆:小堆的根是当前树的最小值,任意子树的根也是当前子树的最小值。

如图:

大根堆:大堆的根是当前树的最大值,任意子树的根也是当前子树的最大值。

如图:

2.4.1.2 堆代码
堆的结构
cpp 复制代码
typedef int HDataType;

typedef struct heap {
	HDataType* arr; // 存储数据
	int size;        // 有效元素个数
	int capacity;    // 最大容量
}Heap;
堆的初始化
cpp 复制代码
void HeapInit(Heap* L) {
	assert(L);

	HDataType* cur = (HDataType*)malloc(sizeof(HDataType) * 4);
	if (cur == NULL) {
		printf("堆开辟失败!!!\n");
		exit(-1);
	}

	L->arr = cur;
	L->size = 0;
	L->capacity = 4; // 初始化就给4块空间
}
堆的判空
cpp 复制代码
int HeapEmpty(Heap* L) {
	assert(L);

	return L->size == 0;
}
堆的销毁
cpp 复制代码
// 堆的销毁
void HeapDestory(Heap** L) {
	assert(*L && (*L)->arr);

	free((*L)->arr);
	free(*L);
	*L = NULL;
}
向上调整建堆

小堆进行向上调整算法要求:调整之前的数据是小堆

大堆进行向上调整算法要求:调整之前的数据是大堆

cpp 复制代码
void Swap(HDataType* a, HDataType* b) {
	assert(a && b);
	HDataType tmp = *a;
	*a = *b;
	*b = tmp;
}
// 向上调整建堆  这里建小堆
void AdjustDown(HDataType* arr, int child) {
	assert(arr);
	 //双亲下标
	 int parent = (child - 1) / 2;
	 // 调整双亲和孩子的位置
	 while (child > 0) {
		 // if中 "<" 换成 ">"就是调整大堆
		 if (arr[child] < arr[parent]) {
			 Swap(&arr[child] , &arr[parent]);
			 child = parent;
			 parent = (parent - 1) / 2;
		 }
		 else {
			 return;
		 }
	 }
}

图形演示:

堆的插入
cpp 复制代码
void HeapInsert(Heap* L , HDataType x) {
	assert(L);

	// 判断容量
	if (L->size == L->capacity) {
		HDataType* tmp = (HDataType*)realloc(L->arr, sizeof(HDataType) * L->capacity * 2);
		if (tmp == NULL) {
			printf("扩容失败,取消插入!\n");
			return;
		}
		L->arr = tmp;
		L->capacity *= 2;
	}

	L->arr[L->size] = x;
	L->size++;
	// 进行向上调整,维持堆结构
	AdjustUp(L->arr, L->size - 1);
}
向下调整建堆

小堆进行向下调整算法要求:根的左子树和右子树是小堆

大堆进行向下调整算法要求:根的左子树和右子树是大堆

cpp 复制代码
// 向下调整建堆 这里建小堆
void AdjustDown(HDataType* arr, int length, int parent) {

	int child = parent * 2 + 1; // 左孩子下标,右孩子下标则+1
	// 调整双亲和孩子
	while (child < length) {
		// 找出两个孩子中的最小值
		if (child + 1 < length && arr[child] > arr[child + 1]) {
			++child;
		}
		// 建大堆">" 换成 "<"
		if (arr[parent] > arr[child]) {
			Swap(&arr[parent], &arr[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else {
			return;
		}
	}
}

图形演示:

堆的删除

堆的删除一般删除堆顶数据,删除堆尾数据无意义

cpp 复制代码
void HeapDelete(Heap* L){
	assert(L && !HeapEmpty(L));

	Swap(&L->arr[0] , &L->arr[L->size - 1]);
	L->size--;
	// 向下调整 维持堆结构
	AdjustDown(L->arr, L->size, 0);
}

2.4.1.3 建堆

建堆,就是把一堆无序数据构建成堆的结构,有时我们需要把一个数组世界构建成一个小堆或者大堆,建堆的方法有两种。

方法一:向上调整算法

思路:先把第0个数看做堆,然后依次把1至n-1位置的数据使用向上调整算法插入堆。

以此类推,直到最后一个元素完成调整。

代码实现:

cpp 复制代码
void HeapInitArray(Heap* L, int* a, int length) {// 堆  给定数组  数组长度
	assert(L && a);
    assert(length > 0);

	HDataType* cur = (HDataType*)malloc(sizeof(HDataType) * length);
	if (cur == NULL) {
		printf("开辟内存失败!!!\n");
		exit(-1);
	}

	L->arr = cur;
	L->size = L->capacity = length;

	memcpy(L->arr, a, sizeof(HDataType) * length);

	for (int i = 1; i < length; i++) {
		AdjustUp(L->arr, i);
	}
}
方法二:向下调整算法

思路:把所有的叶子结点看做是一个个堆,从倒数第二层的最后一个非叶子结点,开始进行向下调整算法建堆,直到调整到根。

代码实现:

cpp 复制代码
void HeapInitArray2(Heap* L, HDataType* a, int length) {
	assert(L && a);
    assert(length > 0);

	HDataType* cur = (HDataType*)malloc(sizeof(HDataType) * length);
	if (cur == NULL) {
		printf("内存开辟失败!!!\n");
		exit(-1);
	}
	L->arr = cur;
	L->size = L->capacity = length;

	memcpy(L->arr, a, sizeof(HDataType) * length);

	for (int i = (L->size - 1 - 1) / 2; i >= 0; i--) {
		AdjustDown(L->arr, L->size, i);
	}
}
时间复杂度对比

向上调整算法时间复杂度:

向下调整算法时间复杂度:

向下调整时间 < 向上调整时间,故一般选择向下调整算法。

测试代码

测试代码是vibcoding出来的,需要测试上述函数的可以用。

cpp 复制代码
/* ==================== Heap test code (appended) ==================== */
/*
 * Test-only code. The heap implementation above is unchanged.
 * Build with MSVC:
 *   cl /nologo /TC /std:c17 /W4 /Zi /Fe:HeapTest.exe Heap.c
 */

static int g_failures = 0;

#define CHECK(cond)                                                     \
    do {                                                                \
        if (!(cond)) {                                                  \
            printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond);      \
            ++g_failures;                                               \
        }                                                               \
    } while (0)

static Heap* NewHeap(void)
{
    Heap* h = (Heap*)malloc(sizeof(Heap));
    if (h == NULL) {
        printf("FAIL: unable to allocate Heap\n");
        exit(1);
    }
    h->arr = NULL;
    h->size = 0;
    h->capacity = 0;
    return h;
}

static int IsMinHeap(const Heap* h)
{
    int i;

    if (h == NULL || h->arr == NULL) {
        return 0;
    }
    if (h->size < 0 || h->capacity < h->size) {
        return 0;
    }
    for (i = 1; i < h->size; ++i) {
        if (h->arr[(i - 1) / 2] > h->arr[i]) {
            return 0;
        }
    }
    return 1;
}

static void CheckPopOrder(Heap* h, const HDataType* expected, int count)
{
    int i;

    CHECK(h->size == count);
    if (h->size != count) {
        return;
    }

    for (i = 0; i < count; ++i) {
        CHECK(h->arr[0] == expected[i]);
        HeapDelete(h);
        CHECK(IsMinHeap(h));
    }
    CHECK(h->size == 0);
    CHECK(HeapEmpty(h) != 0);
}

static void CheckPopSorted(Heap* h)
{
    int have_prev = 0;
    HDataType prev = 0;

    while (h->size > 0) {
        HDataType current = h->arr[0];

        if (have_prev) {
            CHECK(prev <= current);
        }
        prev = current;
        have_prev = 1;

        HeapDelete(h);
        CHECK(IsMinHeap(h));
    }
    CHECK(h->size == 0);
    CHECK(HeapEmpty(h) != 0);
}

static void DestroyHeap(Heap** h)
{
    HeapDestory(h);
    CHECK(*h == NULL);
}

static void TestInitAndDestroy(void)
{
    Heap* h = NewHeap();

    HeapInit(h);
    CHECK(h->arr != NULL);
    CHECK(h->size == 0);
    CHECK(h->capacity == 4);
    CHECK(HeapEmpty(h) != 0);
    CHECK(IsMinHeap(h));

    DestroyHeap(&h);
}

static void TestInsertDelete(void)
{
    const HDataType values[] = { 5, 3, 8, 1, 9, 2 };
    const HDataType expected[] = { 1, 2, 3, 5, 8, 9 };
    int i;
    Heap* h = NewHeap();

    HeapInit(h);
    for (i = 0; i < (int)(sizeof(values) / sizeof(values[0])); ++i) {
        HeapInsert(h, values[i]);
        CHECK(h->size == i + 1);
        CHECK(IsMinHeap(h));
    }
    CHECK(h->capacity == 8);

    CheckPopOrder(h, expected, (int)(sizeof(expected) / sizeof(expected[0])));
    DestroyHeap(&h);
}

static void TestCapacityGrowth(void)
{
    const HDataType expected[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int i;
    Heap* h = NewHeap();

    HeapInit(h);
    for (i = 0; i < 4; ++i) {
        HeapInsert(h, (HDataType)(i + 1));
    }
    CHECK(h->size == 4);
    CHECK(h->capacity == 4);

    HeapInsert(h, 5);
    CHECK(h->size == 5);
    CHECK(h->capacity == 8);

    for (i = 6; i <= 8; ++i) {
        HeapInsert(h, (HDataType)i);
    }
    CHECK(h->size == 8);
    CHECK(h->capacity == 8);

    HeapInsert(h, 9);
    CHECK(h->size == 9);
    CHECK(h->capacity == 16);
    CHECK(IsMinHeap(h));

    CheckPopOrder(h, expected, (int)(sizeof(expected) / sizeof(expected[0])));
    DestroyHeap(&h);
}

static void TestInitArray1(void)
{
    HDataType input[] = { 9, 4, 7, 1, 8, 2, 6, 3, 5 };
    const HDataType expected[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int n = (int)(sizeof(input) / sizeof(input[0]));
    Heap* h = NewHeap();

    HeapInitArray1(h, input, n);
    CHECK(h->arr != NULL);
    CHECK(h->size == n);
    CHECK(h->capacity == n);
    CHECK(IsMinHeap(h));

    CheckPopOrder(h, expected, n);
    DestroyHeap(&h);
}

static void TestInitArray2(void)
{
    HDataType input[] = { 9, 4, 7, 1, 8, 2, 6, 3, 5 };
    const HDataType expected[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int n = (int)(sizeof(input) / sizeof(input[0]));
    Heap* h = NewHeap();

    HeapInitArray2(h, input, n);
    CHECK(h->arr != NULL);
    CHECK(h->size == n);
    CHECK(h->capacity == n);
    CHECK(IsMinHeap(h));

    CheckPopOrder(h, expected, n);
    DestroyHeap(&h);
}

static void TestDuplicatesNegative(void)
{
    HDataType input[] = { -3, 0, -3, 5, 0, -10 };
    const HDataType expected[] = { -10, -3, -3, 0, 0, 5 };
    int n = (int)(sizeof(input) / sizeof(input[0]));
    Heap* h = NewHeap();

    HeapInitArray2(h, input, n);
    CHECK(IsMinHeap(h));

    CheckPopOrder(h, expected, n);
    DestroyHeap(&h);
}

static void TestSingleElement(void)
{
    HDataType input[] = { 42 };
    const HDataType expected[] = { 42 };
    const HDataType expected_after_insert[] = { 7, 42 };
    Heap* h = NewHeap();

    HeapInitArray1(h, input, 1);
    CHECK(IsMinHeap(h));
    CheckPopOrder(h, expected, 1);
    DestroyHeap(&h);

    h = NewHeap();
    HeapInitArray2(h, input, 1);
    HeapInsert(h, 7);
    CHECK(h->size == 2);
    CHECK(h->capacity == 2);
    CHECK(IsMinHeap(h));
    CheckPopOrder(h, expected_after_insert, 2);
    DestroyHeap(&h);
}

static void TestInsertAfterInitArray(void)
{
    HDataType input[] = { 4, 1, 3 };
    const HDataType expected[] = { 0, 1, 3, 4 };
    Heap* h = NewHeap();

    HeapInitArray2(h, input, 3);
    CHECK(h->capacity == 3);

    HeapInsert(h, 0);
    CHECK(h->size == 4);
    CHECK(h->capacity == 6);
    CHECK(IsMinHeap(h));

    CheckPopOrder(h, expected, 4);
    DestroyHeap(&h);
}

static void TestLarge(void)
{
    enum { LARGE_N = 1000 };
    unsigned int seed = 123456789u;
    int i;
    Heap* h = NewHeap();

    HeapInit(h);
    for (i = 0; i < LARGE_N; ++i) {
        seed = seed * 1103515245u + 12345u;
        HeapInsert(h, (HDataType)((int)(seed % 2001u) - 1000));
        if ((i + 1) % 97 == 0) {
            CHECK(IsMinHeap(h));
        }
    }
    CHECK(h->size == LARGE_N);
    CHECK(h->capacity >= LARGE_N);
    CHECK(IsMinHeap(h));

    CheckPopSorted(h);
    DestroyHeap(&h);
}

int main(void)
{
    printf("Running heap tests...\n");

    TestInitAndDestroy();
    TestInsertDelete();
    TestCapacityGrowth();
    TestInitArray1();
    TestInitArray2();
    TestDuplicatesNegative();
    TestSingleElement();
    TestInsertAfterInitArray();
    TestLarge();

    if (g_failures == 0) {
        printf("ALL TESTS PASSED\n");
        return 0;
    }
    printf("%d TEST CHECK(S) FAILED\n", g_failures);
    return 1;
}

2.4.2 堆经典题目

堆排序

思路:无需单独实现完整的堆数据结构(实现繁琐且空间效率低),可以直接将原数组视作完全二叉树,在数组原地完成建堆、选数的全部操作。

规则:对 n 个数做升序排序时,不能构建小堆。原因是小堆仅能确定首位最小值,后续选取次小值时会打乱剩余数据的堆结构,需要全程重建堆,时间代价极高。(降序排序相反)

时间复杂度为,与C++中的 qsort 时间相当。

代码实现:

cpp 复制代码
// 堆排序
void HeapSort(HDataType* a, int length) {
	assert(a);
	if (length <= 1) {
		return;
	}

	//升序排序,建大堆
	for (int i = (length - 1 - 1) / 2; i >= 0;i--) {
		AdjustDown(a, length, i);
	}

	int j = length - 1;
	while (j > 0) {
		Swap(&a[0] , &a[j]);
		AdjustDown(a, j, 0);
			--j;
	}
}

总结:

1、升序排序 → 必须建大堆 :堆顶是全局最大值,方便每次把最大值放到数组末尾。

2、降序排序 → 必须建小堆 :堆顶是全局最小值,方便每次把最小值放到数组末尾。

top-k问题

题目介绍:top-k 是经典算法问题,指从海量数据(数据规模可大到无法一次性全部装入内存)中,快速高效地筛选出最大 / 最小的前 k 个元素,或是出现频率最高的前 k 个元素。

思路:取数组前 k 个元素构建小顶堆;将剩余的 n−k 个元素依次与堆顶比较,若元素大于堆顶则替换堆顶,并向下调整维护堆结构;遍历完成后,堆中留存的就是前 k 大的数据,堆顶元素即为第 k 大的值。

图形演示:

代码实现:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// 小顶堆向下调整函数声明
// a:堆数组,n:堆元素总数,root:调整起始根节点下标
void AdjustDown(int* a, int n, int root);

// 生成10万条随机测试数据,写入data.txt文件
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) % 1000000; // 加i分散数据分布
        fprintf(fin, "%d\n", x);
    }

    fclose(fin);
}

// 小顶堆法求解top-k:找出数据中前k大的元素
// 特点:仅占用O(k)内存,支持无法全量加载的海量数据
void topk() {
    int k = 0;
    printf("请输入k: >");
    scanf("%d", &k);

    // 输入合法性校验
    if (k <= 0) {
        printf("错误:k必须为正整数!\n");
        return;
    }

    const char* file = "data.txt";
    FILE* fin = fopen(file, "r");
    if (fin == NULL) {
        perror("fopen error");
        return;
    }

    // 申请大小为k的堆空间
    int* minheap = (int*)malloc(sizeof(int) * k);
    if (minheap == NULL) {
        perror("malloc error");
        fclose(fin); // 退出前关闭文件,避免资源泄漏
        return;
    }

    // 读取前k个元素作为堆的初始数据
    int readCnt = 0;
    for (int i = 0; i < k; ++i) {
        if (fscanf(fin, "%d", &minheap[i]) != EOF) {
            readCnt++;
        } else {
            break;
        }
    }

    // 边界处理:文件总数据量不足k
    if (readCnt < k) {
        printf("文件共%d条数据,不足%d个:\n", readCnt, k);
        for (int i = 0; i < readCnt; ++i) {
            printf("%d ", minheap[i]);
        }
        printf("\n");
        free(minheap);
        fclose(fin);
        return;
    }

    // 构建小顶堆,时间复杂度O(k)
    for (int i = (k - 2) / 2; i >= 0; --i) {
        AdjustDown(minheap, k, i);
    }

    // 遍历剩余所有数据,筛选前k大
    int x = 0;
    while (fscanf(fin, "%d", &x) != EOF) {
        // 当前元素比堆顶(第k大)大,则替换并维护堆
        if (x > minheap[0]) {
            minheap[0] = x;
            AdjustDown(minheap, k, 0);
        }
    }

    // 输出结果:堆内为前k大元素,堆顶是第k大的值
    printf("前%d大的元素:\n", k);
    for (int i = 0; i < k; ++i) {
        printf("%d ", minheap[i]);
    }
    printf("\n");

    // 释放内存、关闭文件
    free(minheap);
    fclose(fin);
}

其他思路不可行的原因:

**1、**全排序法:对全部数据做降序排序,直接取前 k 个。最优排序时间复杂度为;缺点是必须将所有数据一次性装入内存,无法处理超大规模海量数据。

**2、**全量建大堆法:将全部数据建成大顶堆,连续取 k 次堆顶得到前 k 大元素。时间复杂度远小于 n 时近似,k 接近 n 时退化为;同样依赖全量数据加载到内存,无法处理海量外存数据。


最后,感谢你的阅读,欢迎交流与指正。

相关推荐
BeiGuTing_code1 小时前
Java动态数组(一)
java·数据结构·windows
Interview Aid1122 小时前
Google SWE Intern VO 面经:两轮 Coding,Follow-up 考察 BFS 状态
算法
琳达爱分享2 小时前
从单机令牌桶到集群限流:一次配额超发的复盘
后端·算法
range2 小时前
带TTL的LRU缓存的实现-字节手撕-26/10/19
算法
糖果豆豆2 小时前
n log n 到底从哪来?排序下界、2-SAT 与 3-SAT、停机问题,卡的是同一件事
算法·架构
地平线开发者2 小时前
【玩转 征程6 算法工具链:PTQ 工具篇】hb_verifier
算法·自动驾驶
171320330计算机毕设编程2 小时前
计算机毕业设计 基于springboot+vue的在线观鸟视频播放网站 LW PPT
java·大数据·vue.js·spring boot·算法·课程设计
himobrinehacken3 小时前
位运算揭秘:左右移与符号位陷阱
c语言·c++·算法·安全
INGNIGHT3 小时前
815 · 课程表IV(dfs)
算法·深度优先