이진 트리 및 힙의 간단한 구현

목차

1: 나무란 무엇인가

[1] 나무의 개념

[2] 나무에 대한 몇 가지 다른 중요한 개념

[3] 트리의 여러 표현 방법

2: 이진 트리란 무엇인가

【1】개념 및 특징

[2] 두 개의 특수 이진 트리

[3] 이진 트리의 특성

[4] 이진 트리의 두 가지 저장 방법

3: 힙 구현


1: 나무란 무엇인가

[1] 나무의 개념

앞에서 배운 시퀀스 테이블과 연결 목록은 선형 구조에 속하지만 트리는 n(n>=0)개의 유한 노드로 구성된 계층적 관계의 집합인 비선형 데이터 구조입니다 . 뿌리가 위를 향하고 잎이 아래를 향하는 거꾸로 된 나무처럼 보인다고 하여 나무라고 합니다 .

삽화:

(1) 트리에는 그림에서 노드 A인 루트 노드라는 특수 노드가 있습니다 .

(2) 루트 노드를 제외한 다른 노드들은 M(M>0) 개의 상호 분리된 집합 T1 , T2 , ... , Tm 으로 나뉘며 , 각 집합 Ti(1<= i <= m)는 구조가 A 서브트리이다. 나무와 비슷합니다 . 각 하위 트리의 루트 노드에는 하나의 선행 작업만 있고 0개 이상의 후속 작업이 있을 수 있습니다.

(3) 부모-자식 노드: 예를 들어 A는 B, C, D의 부모 노드이고 E와 F는 B 의 자식 노드입니다.

(4) 리프 노드: E, F, C, G, H 와 같이 자식 노드가 없는 노드.

참고: 트리의 하위 트리는 분리되어 있으며 루트 노드를 제외한 다른 노드에는 부모 노드가 하나만 있습니다.

[2] 나무에 대한 몇 가지 다른 중요한 개념

삽화:

(1) 노드 차수: 노드에 포함된 하위 트리의 수를 노드 차수라고 하며 위 그림과 같이 A 는 6 입니다 .
(2) 비종단 노드 또는 분기 노드: 차수가 0이 아닌 노드, 위 그림과 같이 D , E , F , G... 와 같은 노드 가 분기 노드입니다.
(3) 형제 노드: 부모 노드가 같은 노드를 형제 노드라고 하며 , 위 그림과 같이 B C 는 형제 노드입니다.
(4) 트리의 차수: 트리에서 가장 큰 노드의 차수를 트리의 차수라고 하며 , 위와 같이 트리의 차수는 6 입니다.
(5) 노드의 계층 구조: 루트의 정의에서 시작하여 루트는 첫 번째 레이어이고 루트의 자식 노드는 두 번째 레이어 등입니다.
(6) 트리의 높이 또는 깊이: 트리 노드의 최대 레벨 위의 그림과 같이 트리의 높이는 4 입니다.
(7) 노드의 조상: 루트에서 노드의 가지에 있는 모든 노드까지 , 위 그림과 같이 A 는 모든 노드의 조상입니다.
(8) 자식 및 손자: 특정 노드를 루트로 하는 하위 트리의 모든 노드를 해당 노드의 자손이라고 합니다 . 위에 표시된 것처럼 모든 노드는 A 의 자손 입니다.
(9) 포레스트: m( m>0) 개의 분리된 트리 의 집합을 포레스트라고 합니다 .

[3] 트리의 여러 표현 방법

(1) 시퀀스 테이블을 사용하여 자식 노드의 주소 저장 (복잡한 구조)

(2) 트리의 차수(가장 큰 노드의 차수)를 설명하고 포인터 배열을 설정하여 자식 노드의 주소를 저장

(3) 구조 배열 저장 

 (4) 왼쪽 자식과 오른쪽 형제의 표기법(일반적으로 사용됨, 구조가 비교적 간단하고 논리가 비교적 명료함)

2: 이진 트리란 무엇인가

【1】개념 및 특징

개념:

이진 트리는 비어 있거나 루트 노드와 왼쪽 및 오른쪽 하위 트리라고 하는 두 개의 이진 트리로 구성된 유한 한 노드 집합입니다 .

특징:

1. 각 노드에는 최대 두 개의 하위 트리가 있습니다. 즉, 이진 트리에는 차수가 2보다 큰 노드가 없습니다.
2. 이진 트리의 하위 트리는 좌우로 나뉘며 하위 트리의 순서는 뒤바뀔 수 없습니다.

[2] 두 개의 특수 이진 트리

1. 전체 이진 트리:

모든 리프 노드는 마지막 레이어에 있습니다.

모든 분기 노드에는 두 개의 자식이 있습니다.

2. 완전한 이진 트리:

이 이진 트리에 N개의 레이어가 있다고 가정하면 첫 번째 N-1 레이어는 가득 차 있어야 합니다.

마지막 층은 채워질 수 있지만 왼쪽에서 오른쪽으로 연속적이어야 합니다.

[3] 이진 트리의 특성

1. 루트 노드의 레이어 수가 1로 지정된 경우 비어 있지 않은 이진 트리의 i번째 레이어에는 최대 2^(i-1)개의 노드가 있습니다 .
2. 루트 노드의 레이어 수가 1로 지정된 경우 깊이가 h인 이진 트리의 최대 노드 수는 2^h-1입니다 .
3. 임의의 이진 트리에서 차수가 0이고 리프 노드의 수가 n0 이고 차수가 2인 가지 노드의 수가 n2 이면 n0=n2+1입니다.
4. 루트 노드의 레이어 수를 1로 지정하면 n개의 노드가 있는 전체 이진 트리의 깊이는 h=LogN입니다.

[4] 이진 트리의 두 가지 저장 방법

(1) 순차 구조 저장(어레이)

순차 구조 저장은 저장을 위해 배열을 사용하는 것입니다 . 일반적으로 배열은 완전한 이진 트리를 나타내는 데만 적합합니다 . 완전한 이진 트리가 아닌 경우 공간이 낭비되기 때문입니다. 실제로는 힙만 저장소에 배열을 사용합니다 . 이진 트리 순차 저장소는 물리적으로 배열이고 논리적으로 이진 트리입니다.

(2) 사슬 구조 저장

연결된 목록은 이진 트리를 나타내는 데 사용됩니다 . 즉, 요소의 논리적 관계를 나타내는 데 체인이 사용됩니다.

일반적인 방법은 연결된 목록의 각 노드가 데이터 필드와 왼쪽 및 오른쪽 포인터 필드의 세 필드로 구성되고 왼쪽 및 오른쪽 포인터를 사용하여 왼쪽 자식 및 노드의 오른쪽 자식이 있습니다.

체인 구조는 다시 바이너리 체인과 트리플 체인 으로 나뉘며 현재 바이너리 체인이 일반적으로 사용되며 레드-블랙 트리와 같은 고급 데이터 구조는 트리플 체인을 사용할 것입니다.

3: 힙 구현

힙은 배열로 구현된 이진 트리이며 일반적으로 완전한 이진 트리를 구현하는 데 사용됩니다.

힙은 큰 힙과 작은 힙으로 나뉩니다.

큰 더미: 아버지 >= 자식

작은 더미: 아이 >= 아버지

이 문서는 많은 것을 구현합니다.

힙의 구현은 시퀀스 테이블과 유사하므로 여기에서는 자세히 설명하지 않고 기본 인터페이스 구현만 설명합니다.

시퀀스 테이블 링크 첨부: https://blog.csdn.net/2301_76269963/article/details/129352041?spm=1001.2014.3001.5501

【1】데이터 삽입

(1) 판단 확장은 시퀀스 테이블과 일치합니다.

(2) 시퀀스 테이블과 일치하는 데이터를 저장합니다.

(3) 데이터 삽입 후 삽입 후에도 여전히 큰 더미가 있는지 확인해야 하므로 부모-자식 관계를 조정해야 합니다.

조정을 고려하기 전에 아버지의 첨자와 자녀의 첨자의 관계를 살펴보자. 

우리는 그런 규칙을 찾을 수 있습니다

아버지 첨자 = (자녀 첨자 - 1)/2. 

이 규칙에 따라 조정 함수를 설계하는데, 삽입할 데이터가 부모보다 크면 부모보다 작아지거나 루트 노드가 될 때까지을 교체한다 .

후속 삭제에도 스왑 조정이 필요하기 때문에 스왑을 별도로 HeapSwap( ) 함수로 패키징할 수 있습니다.

암호:

[2] 데이터 삭제(힙 정렬 기준)

기본 아이디어:

(1) 힙의 데이터를 삭제하려면 루트 데이터를 삭제 해야 합니다 .

(2) 원래 힙의 구조를 파괴하는 시퀀스 테이블처럼 직접 덮어쓸 수 없다는 점에 유의 해야 합니다 .

삽화:

(3) 원본 루트 데이터를 삭제하고 조정해야 합니다 .

삭제: 루트를 마지막 리프와 교환한 다음 크기(유효 데이터 수)를 1로 직접 줄일 수 있습니다.

삽화:

 조정: 첨자 0 에서 하향 조정 . 자식이 아버지보다 클 경우 두 자식보다 클 때까지 교체하거나 잎이 조정됨 각 판단 전에 더 큰 두 자식을 비교하여 구조가 파괴되는 것을 방지 큰 힙 , 더 오래된 자식을 부모와 바꾸고 반복합니다.

삽화:

암호:

 

전체 코드:

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* hp);
//销毁
void HeapDestory(HP* hp);
//交换函数
void HeapSwap(int* p1, int* p2);
//判空函数
bool HeapEmpty(HP* hp);
//调整函数
void AdjustUp(HPDataType* a,int child);
//向下调整,n是有效个数
void AdjustDown(HPDataType* a, int n, int parent);
//插入数据
void HeapPush(HP* hp, HPDataType x);
//打印数据
void HeapPrint(HP* hp);
//删除数据
void HeapPop(HP* hp);

Heap.c(인터페이스 구현)

#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
//初始化
void HeapInit(HP* hp)
{
	//断言,不能传空的结构体指针
	assert(hp);
	hp->a = NULL;
	//初始化size和容量都为0
	hp->size = hp->capacity = 0;
}

//销毁
void HeapDestory(HP* hp)
{
	free(hp->a);
	hp->size = hp->capacity = 0;
}

//交换函数
void HeapSwap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//判空函数
bool HeapEmpty(HP* hp)
{
	return hp->size == 0;
}

//调整函数
//向上调整
void AdjustUp(HPDataType* a, int child)
{
	//断言,不能传空指针
	assert(a);
	//找到父结点的下标
	int parent = (child - 1) / 2;
	//循环,以child到树根为结束条件
	while (child > 0)
	{
		//如果父结点比child下,交换并更新
		if (a[child] > a[parent])
		{
			HeapSwap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		//如果父结点比child大,跳出循环
		else
		{
			break;
		}
	}
}
//向下调整
void AdjustDown(HPDataType* a, int n, int parent)
{
	//默认左孩子最大
	int child = parent * 2 + 1;
	//当已经调整到超出数组时结束
	while (child<n)
	{
		//找出两个孩子中大的一方
		//考虑右孩子不存在的情况
		if (child+1<n&&a[child + 1] > a[child])
		{
			//如果右孩子大,child加1变成右孩子
			child++;
		}
		//如果父亲比大孩子小,进行调整,否则跳出
		if (a[child] > a[parent])
		{
			HeapSwap(&a[child], &a[parent]);
			//迭代
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

//插入数据
void HeapPush(HP* hp, HPDataType x)
{
	if (hp->size == hp->capacity)
	{
		//判断扩容多少
		int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		//扩容
		HPDataType* tmp =
			(HPDataType*)realloc(hp->a, sizeof(HPDataType) * newcapacity);
		//更新
		hp->capacity = newcapacity;
		hp->a = tmp;
	}
	//存储数据
	hp->a[hp->size] = x;
	hp->size++;
	//进行调整
	AdjustUp(hp->a, hp->size-1);
}

//打印数据
void HeapPrint(HP* hp)
{
	//断言,不能传空的结构体指针
	assert(hp);
	int i = 0;
	for (i = 0; i < hp->size; i++)
	{
		printf("%d ", hp->a[i]);
	}
	printf("\n");
}

//删除数据
void HeapPop(HP* hp)
{
	//断言,不能传空的结构体指针
	assert(hp);
	//如果为空,不能删除,避免数组越界
	assert(!HeapEmpty(hp));
	//不为空,先交换根和最后一片叶子,然后size减1
	HeapSwap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;
	AdjustDown(hp->a, hp->size, 0);
}

text.c (테스트)

#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"

void text1()
{
	HP hp;
	HeapInit(&hp);
	HPDataType a[] = { 70,30,56,25,15,10.85,79};
	int i = 0;
	for (i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	{
		HeapPush(&hp, a[i]);
	}
	HeapPrint(&hp);
	HeapPop(&hp);
	HeapPrint(&hp);
	HeapDestory(&hp);
}

int main()
{
	text1();
}

추천

출처blog.csdn.net/2301_76269963/article/details/129941907