《算法笔记》数据结构部分(STL&链表&树)

C++ STL

#include <stack>
using namespace std;

//定义一个栈
stack <typename> name;

//stack函数实例
stack.push(x);//将元素x加入到栈中
stack.top();//获取栈顶元素
stack.pop();//将栈顶元素弹出
stack.empty();//判断栈是否为空,返回值类型为空
stack.size();//返回栈内元素

队列

#include <queue>
using namespace std;

//队列的定义
queue< typename > name;

queue<int> Q;
//queue函数实例
Q.push(x);//将元素x入队
Q.front();//获取队首元素
Q.back();//获取队尾元素
Q.pop();//将队首元素出队
Q.empty();//判断队列是否为空
Q.size();//队列中元素的个数

优先队列(Priority_queue)

优先队列就是按照元素的优先级来确定对首部的元素的队列,它是由堆实现的,这种优先级的规则可以自定义。优先队列可以用于计算哈夫曼树最短路径权值,可以对Dijkstra算法进行优化,还可以解决一些贪心问题。

#include<cstdio>
#include<queue>
#include<string>

using namespace std;

int main() {
	priority_queue<int>q;
	q.push(3);//将3入队
	q.empty();//检查队列是否为空
	q.pop();//将优先级最高的元素出队
	q.size();//队列的大小

	//第一个参数是元素类型,第二个是承载底层数据结构堆的容器,第三个是参数的比较类,less<int>表示数字越大优先级越高
	priority_queue<int, vector<int>, greater<int>>q_1;

}

//当想以结构体中的数据作为优先级的时候,应该堆操作符进行重载
//使用时直接将相应的结构体定义的变量直接入队即可
struct fruit {
	string name;
	int price;
	
	//价格高的优先级高
	//由于队列默认将优先级高的放在队首,如果将小于号重载为大于号,最后会将规则反向
	friend bool operator < (fruit f1, fruit f2) {
		return f1.price < f2.price;
	}

};

//也可以将比较函数写在结构体外面
struct cmp{	
	bool operator()(fruit f1, fruit f2) {
		return f1.price > f2.price;
	}
};
priority_queue<fruit, vector<int>, cmp>p_2;

//当结构体数据较大应该使用引用来提高效率
/*
friend bool operator < (const fruit& f1, const fruit& f2) {
	....
}
*/

pair采用方法

pair可以用于快速定义一个相当于拥有两个变量的结构体。使用pair需要添加函数头utility.当然map函数头中包含前者。

//定义一个pair变量,并初始化
pair<typename1, typename2>name(initial1, initial2);
//定义一个临时pair变量
name = make_pair(initial1, initial2);

pair函数的操作

比较两个pair函数:可以直接用比较符,规则是先比较第一个的大小,当相等时比较第二个元素的大小
常见用途:1.代替二元结构体机器构造函数。2.作为map的键值进行插入。

#include<iostream>
#include<string>
#include<map>
using namespace std;

int main(){
	map<string, int>mp;//定义一个哈希表
	//往哈希表中插入两个对键值
	mp.insert(make_pair("hey", 5));
	mp.insert(pair<string, int>("Hei", 10));
	//利用迭代器遍历哈希表
	for(map<string, int>::iterator it = mp.begin();it != mp.end();++it){
		cout<<it->first << " " << it->second << endl;
	}
	return 0;
}

向量

需要包含文件头vector 和using namespace std;
定义方式:

vector<typename> vector_name;

操作:
1.获取元素的最后一个元素

vector_name.end()-1;//注意end是指向最后一个元素的下一位
vector_name.back();
vector_name.rbegine();
vector_name.at(vector_name.size()-1);

集合set

集合是一种内部有序且不含重复元素的容器。当数据加入到集合中是,会自动去重和排序,默认排序是从小到大。要使用集合需要加上set函数头,并且使用std名字空间。

定义方式:(其实容器定义的方式都大同小异)

迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符, − > 、 ∗ 、 + + 、 − − ->、*、++ 、-- >++等封装了指针

set<typename> name;

访问方式:(集合只能通过迭代器访问)

//定义迭代器
set<typename>::iterator it;

//除了vector和string之外,其他的STL都不支持*(it+1)的访问方式
//枚举
set<int>st;
//initialize
.......
for(set<int>::iterator it = st.begin(); it != st.end();it++){
	visit(*it);
}

set常用的函数:

insert(x);//将x加入set,并实现递增排序和去重(O(logN))
find(value);//找到对应value的迭代器(O(logN))

erase(it);//删除迭代器所指的元素,可以通过find函数找到对应的迭代器
erase(value);//删除值为value的元素。时间复杂度O(logN)
erase(first, last);//删除区间元素,删除区间为[first, last)

size();//set中元素的个数O(1)
clear();//delete all elements within the set 

使用unordered_set,只进行去重而不进行排序,速度比set要快很多。

字符串类型String

string类型将字符串数组的一些常用操作封装起来,使用时需要添加文件头string(注意一点string和string.h是不一样的),还需要std的名字空间。
初始化

//define & initializing
string str = "abc";

访问

//Access
//1.By subindex
str[1];

想要输入或者输出字符串,只能使用cin和cout。需要用printf输出,可以使用c_str()函数将string类型转换成字符串数组

printf("%s\n",str.c_str());

通过迭代器访问

//define. It no needs typename 
string::iterator it;
for(string::iterator it = str.begin(); it != str.end();++it){
	printf("%c", *it);
}

string和vector一样,能够直接通过对迭代器加减某数字实现访问。

str.begin() + 3;

函数实例

//连接两string类型
str3 = str2 + str1;
str1 += str2;

//按照字典序比较两string类型的大小
str1 >= str2;

//返回string类型的长度
str.size();
str.length();

insert()函数

str.insert(pos,string);//在str的第pos位插入stringO(N)

//str的3号位插入str2
(prototype->)insert(it,it2,it3);//串[it2, it3)插入到it对应的位置上
str.insert(str.begin() + 3, str2.begin(), str2.end());

erase()删除

//删除单个元素
erase(it);//删除元素的迭代器
erase(fist,last);//删除区间[first, last)的元素
erase(pos,length);//pos开始删除length个字符个数
clear();//清空

其他

substr(pos,len);//冲pos开始获取长度为len的子串
string::npos; //一个常数等于-1或者是unsigned_int的最大值,用于find函数失配的返回值
find(str2);//找到str2在字符串中第一次出现的位置,找不到返回string::npos.时间复杂度O(nm)

str.replace(pos, len, str2);//将str从pos位开始长度为len的子串替换成str2
str.replace(it1,it2,str2);//把迭代器范围内[it1,it2)的子串换成str2

链表

链表的概念

链表的物理地址是可以是不连续的,但是链表逻辑上是连续的。链表包含两个部分,一个用于存储数据,一个用于存储下一个节点的内存地址,以保证逻辑上的连续。

//链表的结构体定义
struct node{
	typename data;//数据域
	node* next;//指针域
};

链表节点的空间分配

为链表节点分配空间可以利用两种函数:malloc(C语言) 和new(C++)。但是在空间分配之后,需要队空间进行释放,否则在一些情况下就会导致内存泄漏(在一些大型程序中,可能会导致弃用的空间一直被占用,而使得无法申请新的空间)。

malloc函数
#include<stdlib.h>

typename* p = (typename*)malloc(sizeof(typename));
/*整个过程就是malloc函数申请一个于数据类型大小一致的空间,这个时
候返回的指针是void类型,然后将他强制转化成int类型,并分配给
typename指针。申请失败的时候会返回空指针
*/

//释放内存
free(p);
//本质上就是将p指针指向空地址,原先的地址占用状态被修改

new运算符(推荐使用)
//申请一个节点空间
typename* p = new typename;

//取消对该节点的占用
delete(p;

链表的基本操作

创建链表(运用for语句实现)
//传入的数组为每个节点的数据域的值
//尾插法:需要一个链表最后一个节点的指针。
node* create (int Array[]){
	node *p, *pre, *head;
	head = new node;//创建头节点
	head->next = NULL;
	pre = head;
	
	for(int i = 0; i < 5; ++i){
		//创建新结点:1.申请空间;2.完成指针域和数据域的赋值
		p = new node;
		p->data = Array[i];
		p->next = NULL;
		
		//更新状态指针
		pre->next = p;
		pre = p;
	}
	return head;
]
//注意一点在删除一个节点的时候需要有一个指向该节点的指针,以便后续删除该节点

静态链表

静态链表的主要原理就是Hash。它利用数组的下标作为相应节点的地址,适用于节点地址是比较小的整数。

//静态链表节点的定义方式
struct Node{
	typename data;
	int next;//注意这里的地址是一个整型变量
}node[size];

需要注意一点:因为静态链表是由数组实现的,所以就可能需要对其进行排序,但是如果这时候结构体类型名和结构体变量名相同(是允许的),sort函数就与出现编译错误。
(例题:A1032)
静态链表的使用步骤:

//定义静态链表
struct Node{
	int address;
	typename data;
	int next;
	XXX;//节点的某些性质
}node[MAXN];

//初始化静态链表:将性质量定义为正常情况达不到的数字
for(int i = 0; i < MAXN; ++i){
	node[i].XXX = 0;
}

//标记(针对性质操作)
//可以针对性质进行统计,标记
int p = begin, count = 0;//用于开始遍历和统计
while(p != -1){
	XXX = 1;
	count++;
	p = node[p]->next;
}

//简化操作
//由于静态链表直接采用地址映射的方式,这可能会导致地址
//不是连续的,此时应该将有效节点转移到左端,可以通过排
//序(sort函数)来实现(结合初始化的值)

搜索

深度优先搜索(DFS)

深度优先遍历会优先解决一种情况下的所有情况之后,再去解决其他的情况。如果能将一个事件的各个情况写成树形结构,就可以很容易地通过这种方法解决。

/*
有n 件物品,每件物品的重量为w[i],价值为c[i]。现在需要选出若干件物品放入一个容
量为V 的背包中,使得在选入背包的物品重量和不超过容量V 的前提下,让背包中物品的价
值之和最大,求最大价值。(l<n<20)
*/
#include <cstdio>

const int maxn = 30;
int n;//物品数量
int V;//背包容量
int maxValue = 0;//最大的价值
int w[maxn], c[maxn];//分别为单个物品的重量个价值

//参数分别为物品编号,总质量,总价值
void DFS(int index, int sumW, int sumC){
	if (index == n) {//完成对所有物品的选用
		if (sumW <= V && sumC > maxValue) {//检查当前方案是否满足条件
			maxValue = sumC;//更新最大价值
		}
		return;
	}
	//对index号的物品,有两种结果,放入背包或是不放入背包
	DFS(index + 1, sumW, sumC);//不放入背包
	DFS(index + 1, sumW + w[index], sumC + c[index]);//放入背包

}

int main() {
	scanf("%d%d", &n, &V);
	for (int i = 0; i < n; ++i) {
		scanf("%d", &w[i]);//重量
	}
	for (int i = 0; i < n; ++i) {
		scanf("%d", &c[i]);//价值
	}
	DFS(0, 0, 0);//从第零件物品开始,此时的价值和重量都是0
	printf("%d\n", maxValue);
	return 0;
}

注意到在加入index号的物品时,总重量发生变化,这可能会导致结果不满足条件,如果能在进入递归之前能够进行检查,能够减少一些不必要的递归

//在递归之前加上判断条件
void DFS_A(int index, int sumW, int sumC) {
	if(index == n)return;
	DFS_A(index + 1, sumW, sumC);
	//检查质量是否满足要求,以确定是否进入递归
	if (sumW + w[index] <= V) {
		if (sumC + c[index] > maxValue)maxValue = sumC + c[index];
		DFS_A(index + 1, sumW + w[index], sumC + c[index]);
	}
}

深度优先遍历的方法可以用于解决获取最优“子序列”的问题,也就是满足基本条件下的最优序列。

/*
问题描述:给定N个整数,从中选择K个数,使这K个数之和恰好等于给定的整数X,求出元素
平方和最大的结果
算法思想:利用一个数组,将已经选择的整数放入其中,当选择index号数字时,将
这个数字加入到数组中,进入递归,之后再将这个数字取出,进入不选择index号数
的递归。
*/

int n, k, x, maxSumSqu = -1, A[maxn];
vector<int>temp, ans;
void DFS_BSqu(int index, int nowK, int sum, int sumSqu) {
	if (nowK == k && sum == x) {
		if (sumSqu > maxSumSqu) {
			maxSumSqu = sumSqu;
			ans = temp;
		}
		return;
	}
	if (index == n || nowK > k || sum > x)return;
	temp.push_back(A[index]);
	DFS_BSqu(index + 1, nowK + 1, sum + A[index], sumSqu + A[index] * A[index]);
	temp.pop_back();
	DFS_BSqu(index + 1, nowK, sum, sumSqu);
}

广度优先

广度优先类似于先解决简单的问题(离起点最近)然后再一步步深入。广度优先会先遍历所有的情况,对于求最优解的情况有优势。

/*
问题描述:给定一个n*m大小的迷宫,其中*代表不可通过的墙
壁,而“.”代表平地,S表示起点,T代表终点。移动过程中,如
果当前位置是(x,y)(下标从0开始),且每次只能往前上下
左右移动,求从起点到终点的最小步数。
算法思想:可以将迷宫当作树型结构,将迷宫的起点作为树的
根节点现在问题转换成找到迷宫终点所在树的层数
*/
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int maxn = 100;

struct node
{
	int x, y;//记录当前的位置
	int step;//步数(层数)
}S,T,Node;//分别为起点,重点,临时节点

int n, m;//分别为行,列
char maze[maxn][maxn];//迷宫信息
bool inq[maxn][maxn] = { false };//检查此位置是否遍历过

//增量数组,XY组合表示一个节点上下左右四个方向的位置
int X[4] = { 0,0,1,-1 };
int Y[4] = { 1,-1,0,0 };

//检查(x,y)这个位置是否应该入队
bool test(int x, int y) {
	//超出边界
	if (x >= n || x < 0 || y >= m || y < 0)return false;
	//遇到墙壁
	if (maze[x][y] == '*')return false;
	//检查是否入队。入队则是之前访问过的位置
	if (inq[x][y] == true)return false;
	return true;//表示该位置有效
}

int BFS() {
	queue<node>q;
	q.push(S);//将起点加入到队列中

	//判断当前访问的位置(出队的位置)是否为终点
	while (!q.empty()) {
		node top = q.front();
		q.pop();
		//该位置为终点,直接返回此时的深度
		if (top.x == T.x && top.y == T.y) {
			return top.step;
		}

		//检查该位置4个方向上的情况
		for (int i = 0; i < 4; ++i) {
			int newX = top.x + X[i];
			int newY = top.y + Y[i];

			if (test(newX, newY)) {
				//新节点,更新相关的信息
				Node.x = newX, Node.y = newY;
				Node.step = Node.step + 1;
				q.push(Node);
				inq[newX][newY] = true;
			}
		}
	}
	return -1;
}

int main(void) {
	//迷宫规格
	scanf("%d%d", &n, &m);
	//传入迷宫信息
	for (int i = 0; i < n; ++i) {
		getchar();
		for (int j = 0; j < n; ++j) {
			maze[i][j] = getchar();
		}
		maze[i][m + 1] = '\0';
	}
	scanf("%d%d%d%d", &S.x, &S.y, &T.x, &T.y);
	S.step = 0;
	printf("%d\n", BFS());
	return 0;
}

值得注意的一点,当将一个元素push入队的时候,其本质就是将该元素的一个副本入队,所以,队列中的元素和原数据来源是相互独立的。

二叉树

二叉树可以通过递归的方式定义,也就是可以将二叉树的子树看作一个新的二叉树。注意一点,二叉树的左右节点是由严格区分的。

二叉树的存储和操作

//二叉树结构体的定义
struct node{
	typename data;
	node* lchild;
	node* rchild;
};

node* root = NULL;//建树之前根节点不存在,将其设为NULL

//新建节点:申请空间,赋值,初始化,返回节点地址
node* newNode(int v){
	node* Node = new node;
	Node->data = v;
	Node->lchild = Node->rchild = NULL;
	return Node;
}

//搜索节点:检查特殊情况和满足条件的情况,然后用同样的方法检查其他的节点
void search(node* root, int x, int newdata){
	if(root == NULL)return;
	if(root->data == x)root->data = newdata;
	search(root->lchild, x, newdata);
	search(root->rchild, x, newdata);
}

//二叉树节点的插入,注意这里输入的参数是引用型的,因为要对这个树进行修改
void insert(node* &root, int x){
	//查找失败,这就是要插入节点的地方
	if(root == NULL){
		root = newNode(x);
		return;
	}
	if(根据条件检查检索的方向)insert(root->lchild,x);
	else insert(root->rchild,x);
}

//二叉树的建立
node* Create(int data[], int n){
	node* root = NULL;
	for(int i = 0; i < n; ++i){
		insert(root,data[i]);
	}
	return root;
}

二叉树的遍历

在这里插入图片描述

/*中序遍历(非递归):先找到最左边的节点,在寻找的过程中将途中的节点加入到栈中,当遇到根节点的时候,
取出栈顶元素(这一步相当于回溯),访问其右节点,然后继续原先的循环
*/
void InOrderWithoutRecursion2(BTNode* root)
{
	//空树
	if (root == NULL)
		return;
	//树非空
	BTNode* p = root;
	stack<BTNode*> s;
	while (!s.empty() || p)
	{
		if (p)
		{
			s.push(p);
			p = p->lchild;
		}
		else
		{
			p = s.top();
			s.pop();
			cout << setw(4) << p->data;
			p = p->rchild;
		}
	}
}
/*
先序遍历(非递归):在访问头节点的同时先将右节点加入到栈中,然后再继续访问左子树。当没有左孩子的时候
访问右节点,表现为取出栈顶元素,进行访问
*/
void PreOrderWithoutRecursion(BTNode* root)
{
	if(root == NULL)return;
	stack<BTNode*>s;
	BTNode* p = root;
	s.push(root);
	while(!s.empty())
	{
		visit(p);
		if(p->rchild)s.push(p->rchild);
		
		if(p->lchild)p = p->lchild;
		else{//访问右子树
			p = s.top();
			s.pop();
		}
	}
}
/*
后序遍历(非递归):后序遍历有一个问题,需要当前访问的节点是父节点的左孩子还是右孩子,如果是左孩子,就访问
右孩子,否则访问头节点。
*/
void postOrderWithoutRecursion(BTNode* root)
{
	if(root == NULL)return;
	stack<BTNode*>s;
	BTNode* pCur, *pLastVisit;

	pCur = root;
	pLastVisit = NULL;
	
	//Find out the leftmost element of the tree.
	while(pCur){
		s.push(pCur);
		pCur = pCur->lchild;
	}
	while(!s.empty())
	{
		pCur = s.top();
		s.pop();

		//访问头节点的条件:无右孩子或者是右孩子已经被访问
		if(pCur->rchild == NULL || pCur->rchild == pLastVisit){
			visit(pCur);
			pLastVisit = pCur;
		}
		else{
			s.push(pCur);
			pCur = pCur->rchild;
			
			//找到右子树的最左边的节点
			while(pCur){
				s.push(pCur);
				pCur = pCur->lchild;
			}
		}
	}
}

//递归版
void xOrder(node* root){
	if(root == NULL)return;
	
	//'* = printf("%d\n",root->data);
	*//前序遍历
	xOrder(root->lchild);
	*//中序遍历
	xOrder(root->rchild);
	*//后序遍历
}

//层次遍历:每次完成一个节点的遍历,就需要检查其是否为叶子节点,否则加入到队列中
void LayerOrder(node* root){
	queue<node*>q;
	q.push(root);
	while(!q.empty()){
		node* now = q.front();
		q.pop();
		printf("%d",now->data);
		if(now->lchild != NULL)q.push(now->lchhild);
		if(now->rchild != NULL)q.push(now->rchild);
	}
}

//如果还想要直到当前节点所在的层数,可以在结构体中做好标记
struct node{
	int data;
	int layer;
	node* lchild;
	node* rchild;
};

void LayerOrder(node* root){
	queue<node*>1;
	root->layer = 1;
	q.push(root);
	
	while(!q.empty()){
		node* now = q.front();
		q.pop();
		printf("%d",now->data);
		
		//更新节点信息之后,再将节点入队
		if(now->lchild != NULL){
			now->lchild->layer = now->layer + 1;
			q.push(now->lchild);
		}
		if(now->rchild != NULL){
			now->rchild->layer = now->layer + 1;
			q.push(now->rchild);
		}
	}
}

树的静态实现(非二叉树)

实现静态写法是为了避免指针可能带来的错误。主要的思想就是将树的孩子节点存储在一个向量(“变长数组”)中。

struct node{
	typename data;
	vector<node> child;//??
}Node[maxn];
//如果不需要数据域,可以用如下方法定义
vector<int>child[maxn];//这里定义了maxn个向量

树的遍历

//先序遍历:先访问节点,之后递归地遍历其孩子节点
void PreOrder(int root){
	visit(root);
	for(int i = 0; i < Node[root].child.size();++i){
		PreOrder(Node[root].child[i]);
	}
}
//层序遍历:先访问头节点,然后将孩子节点入队,直到队列为空
void LayerOrder(int root){
	queue<int>Q;
	Q.push (root);
	while(!Q.empty()){
		int front = Q.front();
		Q.pop();
		visit(Q);
		//下面是访问该节点所有孩子节点的方法
		for(int i = 0; i < Node[front].child.size();++i){
			Q.push(Node[front].child[i]);
		}
	}
}

二叉查找树

二叉查找树是节点按照一定规律排列的树,根节点的数据域大于或者小于左右孩子节点的数据域。同时二叉查找树还可以是一个空树。

//二叉查找树的操作

//新建一个节点:申请空间,赋值初始化,返回节点地址
node* newNode(int v){
	node* Node = new node;
	Node->data = v;
	Node->lchild = Node ->rchild = NULL;
	return Node;
}

//查找二叉查找树中数据域为x的节点
void search (node* root, int x){
	//递归出口
	if(root == NULL){
		printf("search failed\n");
		return;
	}
	//根据当前节点值得情况,决定递归方向
	if(x == root->data)printf("%d\n",root->data);
	else if(x < root->data) search(root->lchild, x);
	else search(root->rchild, x);
}

//插入一个数据域为x的新节点(注意这里的root是需要引用,后续修改)
void insert (node* &root, int x){
	//当查找失败的时候,这个地方即为节点插入的位置
	if(root == NULL){
		root = newNode(x);
		return;
	}
	//由数据域的情况决定递归的方向
	if(x == root->data)return;
	else if(x < root->data)insert(root->lchild, x);
	else insert(root->rchild, x);
}

//二叉查找树的建立
node* Crate(int data[], int n){
	node* root = NULL;
	//将数组数据找到一个合适的地方插入
	for(int i = 0; i < n; ++i)insert(root,data[i]);
	return root;
}

//二叉查找树的删除
//可以用根节点的前去或者后继来代替被删除的头节点

//找到极值点的过程就是不断向左或者向右的过程,直到遇到空节点
//找到以root为头节点的最大全职的节点
node* findMax(node* root){
	while(root->child != NULL)root = root -> rchild;
	return root;
}
//找到权值最小的节点
node* findMin(node* root){
	while(root->lchild != NULL)root = root->lchild;
	return root;
}

//删除节点:想用前驱后者后继节点覆盖要删除的节点,然后问题就转换成删除前驱或后继节点
//先找到想要删除的节点,然后判断节点的类型,然后再进行相应的递归操作
//删除以root为根节点的权值为x的节点
void deleteNode(node* &root, int x){
	if(root == NULL)return;
	//找到需要删除的节点
	if(root->data == x){
		//当需要删除的节点为叶节点,直接删除
		if(root->lchild == NULL && root->rchild == NULL)root = NULL;
		
		//左孩子非空,则用右孩子的极值替换该节点,然后删除该极值节点
		else if(root->lchild != NULL){
			node* pre = findMax(root->lchild);
			root->data = pre->data;
			deleteNode(root->lchild,pre->data);
		}
		
		//右孩子非空,操作同上
		else{
			node* next = findMin(root->rchild);
			root->data = next -> data;
			deleteNode(root->rchild, next->data);
		}
	//寻找需要删除的节点
	else if(root->data > x)deleteNode(root->lshild,x);
	else deleteNode(root->rchild, x);
	}	
}

对于删除节点的操作,可以进行优化,主要里利用极值节点坑定没有左子树或右子树的性质,将该极值节点的子树直接连接到该极值节点的父节点上即可。
加入一直删除前驱或者后继,就会导致最后树变得十分不平衡,解决这个方法可以采取交替删除前驱后继或者是记录子树高度,优先在高度更高的的子树中删除节点。

平衡二叉树

平衡二叉树是左右子树高度相差不超过1的二叉树。这样的结构可以保证在大多数操作下保持O(logn)的性能。因为当二叉树严重失衡时,二叉树退化成链表,此时操作的时间复杂度变为O(n)。在平衡二叉树中,右子树与左子树的高度之差称为该节点的平衡因子

//节点结构体
struct node{
	int v, height;//节点权值和子树高度
	node* lchild, *rchild; 
};

//生成一个新的节点
node* newNode(int v){
	node* Node = new node;//申请节点空间
	Node->v = v;//节点权值
	Node->height = 1;//该节点本身的高度
	Node->lchild = Node->rchild = NULL;//初始化孩子节点
}

//获取当前树的高度:从结构体变量中获取
int getHeight(node* root){
	if(root == NULL)return 0;
	return root->height;
}

//计算平衡因子:左子树高度减去右子树高度
int getBalanceFactor(node* root){
	return getHeight(root->lchild) - getHeight(root->rchild);
}

//更新当前节点的高度:孩子节点的最大高度加上节点本身
void updateHeight(node* root){
	root->height = max(getHeight(root->lchild), getHeight(root->rchild)) + 1;
}

//ALV的查找操作与二叉排序树的操作完全一致

对于ALV的插入操作,要根据树的结构做出相应的不同的操作。(BF:Balance Factor)

树型判定条件调整方法
LLBF(root) = 2;BF(root->lchild) = 1对root进行右旋
LRBF(root) = 2;BF(root->lchild) = -1先对root->lchild进行左旋后对root进行右旋
RRBF(root) = -2;BF(root->lchild) = -1对root进行左旋
RLBF(root) = -2;BF(root->lchild) = 1先对右子树进行右旋,再对root左旋

通过表格,可以总结:L代表根节点是正数,R代表孩子节点的负数。
下图时LL和LR型的树的调整示意图
在这里插入图片描述
在这里插入图片描述
注意到LR型是将LR先左旋转换成LL型的二叉树然后再执行右旋的,所以LR型的二叉树的调整可以由基本的左旋右旋组合而来。

//左旋:将右孩子的左孩子连接到头节点处,其原先的左孩子连接到原先的头节点的右孩子处
void L(node*& root){
	node* temp = root->rchild;
	root->rchild = temp->lchild;
	temp->lchild = root;
	//跟新调整之后的节点高度
	updateHeight(root);
	uodateHeight(temp);
	root = temp;
}

//右旋
void R(node*& root){
	node* temp = root->lchild;
	root->lchild = temp->rchild;
	temp->rchild = root;
	updateHeight(root);
	uodateHeight(temp);
	root = temp;
}

//插入节点:找到插入的位置,判断插入后树的类型,然后再做相应的操作
void insert (node*&root,int v){
	if(root == NULL){
		root = newNode(v);
		return;
	}
	//节点小,则在左子树中插入
	if(v < root->v){
		insert(root->lchild,v);
		//更新节点信息,
		updateHeight(root);
		//L
		if(getBalanceFactor(root) == 2){
			//LL
			if(getBalanceFactor(root->lchild)==1) R(root);
			//LR
			else if(getBalanceFactor(root->lchild)==-1){
			L(root->lchild);
			R(root);
		}
	}
	else{
		//从右子树中插入节点
		insert(root->rchild,v);
		updateHeight(root);
		//R
		if(getBalanceFactor(root) == -2){
			//RR
			if(getBalanceFactor(root->rchild)==-1)L(root);
			//RL
			else if(getBalanceFactor(root->rchild) == 1){
				R(root->rchild);
				L(root);
			}//else if
		}//if
	}//else
}
//AVL树的建立
node* Create(int data[], int n){
	node* root = NULL;
	for(int i = 0; i < n; ++i)insert(root,data[i]);
	return root;
}

并查集

在计算机科学中,并查集(英文:Disjoint-set data structure,直译为不交集数据结构)是一种数据结构,用于处理一些不交集(Disjoint sets,一系列没有重复元素的集合)的合并及查询问题。并查集支持如下操作:
查询:查询某个元素属于哪个集合,通常是返回集合内的一个“代表元素”。这个操作是为了判断两个元素是否在同一个集合之中。
合并:将两个集合合并为一个。
添加:添加一个新集合,其中有一个新元素。添加操作不如查询和合并操作重要,常常被忽略。

以下是并查集得一些基本操作。

const int N = 10;
int father[N];
//初始化
void initialSet(int father[], int N) {
	for (int i = 1; i <= N; ++i) {
		father[i] = i;
	}
}

//查找:根节点的父节点就是其本身
int findFather(int x) {
	while (x != father[x]) {
		x = father[x];
	}
	return x;
}
//递归版本
int findFatherRe(int x) {
	//递归出口
	if (x == father[x])return x;
	else return findFather(father[x]);
}

//并查集合并:先判断两节点是否再同一并查集中(判断根节点是否相同),然后再将其中一个根节点的父节点指向另一个根节点
//这样能能够保证并查集是个树型结构,不会出现环
void Union(int a, int b) {
	int faA = findFather(a);
	int faB = findFather(b);

	//当二者不是统一并查集时合并
	if (faA != faB){
		father[faA] = father[faB];
	}
}

//路径压缩:将搜索的时间复杂度由O(n)变为O(1),其实就是将所有节点的父节点全部指向根节点
int findFatherPatheShorten(int x){
	int a = x;//找到根节点,同时保存初始节点

	//找到根节点
	while (x != father[x]) {
		x = father[x];
	}
	//将该节点的父节点指向根节点,并保存该节点原先的父节点的信息
	while (a != father[a]) {
		int z = a;
		a = father[a];
		father[z] = x;
	}
	return x;
}

//递归版本
int findFatherRSRec(int v) {
	if (v == father[v])return v;
	else{
		int F = findFather(father[v]);
		father[v] = F;
		return F;
	}
}

堆本质上就是满足父节点大于(或小于)孩子节点的二叉树。所有节点存储在一个数组当中,这样i节点的孩子节点分别为 2 ∗ i 2*i 2i 2 ∗ i + 1 2*i+1 2i+1

const int maxn = 100;

int heap[maxn], n = 10;

void swap(int &a, int &b) {
	int temp = a;
	a = b;
	b = temp;
}

//向下调整O(logn):从low到high。找出该节点得该子节点中最大得节点,然后于该节点进行比较,若孩子节点较大,则与孩子节点进行交换
void downAdjust(int low, int high) {
	int i = low, j = i * 2;
	while (j <= high) {
		//找出孩子节点中较大的
		if (j + 1 <= high && heap[j + 1] > heap[j]) ++j;

		//与父节点进行比较
		if (heap[j] > heap[i]) {
			swap(heap[j], heap[i]);
			i = j;
			j = i * 2;//这里只去检查被调整的元素是否满足堆的条件,因为为改动的节点应该是满足堆的要求的
		}
		else{
			break;
		}
	}
}

//建堆O(n):从最后一个非叶节点往后进行循环,这是对原先数组进行了排序,以满足堆的要求
void createHeap() {
	for (int i = n / 2; i >= 1; --i)downAdjust(i, n);
}

//删除堆顶元素O(logn)
void deleteTop() {
	//将最后一个元素代替堆顶元素,然后减少节点数量
	heap[1] = heap[n--];
	downAdjust(1, n);//改动了这个节点就从这个节点进行调整
}

//向上调整:当插入一个节点的时候,这个节点被放到最后,逐个与其父节点比较,直到该节点小于父节点
void upAdjust(int low, int high){
	int i = high, j = i / 2;//high就是最后一个节点,即插入的节点

	//父节点在范围之内
	while (j >= low) {
		if (heap[j] < heap[i]) {
			swap(heap[j], heap[i]);
			i = j;
			j = i / 2;
		}
		else
		{
			break;
		}
	}
}

//添加元素,借用上面的函数:加入节点,然后进行相应的调整
void insert(int x) {
	heap[++n] = x;
	upAdjust(1, n);
}

//堆排序:将头节点与最后一个节点进行交换,然后对堆进行调整
void heapSort() {
	createHeap();
	for(int i = n; i > 1; i--){
		swap(heap[i], heap[1]);
		downAdjust(1, i - 1);
	}
}

哈夫曼树

哈夫曼树构建过程
用语言描述就是,先将队列中权值最小的两个节点合并成新的节点,加入到队列中,然后一直按照这个规律执行,直到队列中只剩下一个节点。这个时候头节点的权值就是最短路径权值

//以下是求一个实例的最短路径的权值的算法
#include<cstdio>
#include<queue>

using namespace std;

priority_queue<long long, vector<long long>, greater<long long>> q;

int main() {
	int n;
	long long temp, x, y, ans = 0;
	scanf("&d", &n);
	for (int i = 0; i < n; ++i) {
		scanf("%lld", &temp);
		q.push(temp);
	}

	while (q.size()>1)
	{
		//获取前两个最大节点
		x = q.top();
		q.pop();
		y = q.top();
		q.pop();

		//将节点合并之后再入队
		q.push(x + y);
		//统计最短路径权值
		ans += x + y;

	}
	printf("%lld", ans);
	return 0;
}

哈夫曼编码是一种用于无损数据压缩的熵编码(权编码)算法,它可以根据数据的一些特点(如出现频率)来制定相应长度的编码从而达到数据压缩的目的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++ STL(Standard Template Library)是C++标准库中的一部分,它提供了一系列的模板类和函数,用于实现常见的数据结构算法数据结构算法是计算机科学中的基础概念,用于解决各种问题。 区别在于: 1. 覆盖范围:C++ STL是一个广泛的库,提供了许多常用的数据结构算法,如向量、链表、堆、哈希表、排序算法等等。而数据结构算法是计算机科学的核心概念,涵盖了更广泛的范围,包括了更多的数据结构算法的设计与实现。 2. 实现方式:C++ STL提供了模板类和函数的封装,可以直接使用这些类和函数来实现数据结构算法。而数据结构算法需要在具体的编程语言中进行实现,可以使用C++或其他编程语言来实现。 3. 使用方式:C++ STL提供了一种方便、高效的方式来使用常见的数据结构算法,可以大大简化程序的开发和维护。而数据结构算法需要开发者自己设计和实现,需要一定的算法知识和编程能力。 4. 扩展性:C++ STL是一个标准库,提供了一些常用的数据结构算法,但不一定满足所有的需求。而数据结构算法的实现可以根据具体的需求进行扩展和优化。 总的来说,C++ STL是一个方便、高效的工具,可以快速实现常见的数据结构算法。而数据结构算法是一门学科,需要深入理解和掌握,能够根据具体的问题进行设计和实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值