二叉树高频题--part1(不含树型dp)

树型dp简介

一,二叉树的层序遍历

把树按一层一层的搜索,访问。

这个问题里我们要放回一个链表,其包含该层所有节点。

之前设计不使用递归实现前后中序二叉树遍历的算法,就是使用栈来完成,用栈来模拟递归函数。

现在我们使用队列,利用先进后出,弹出的节点后,又将它的子节点加入队列中,这样只有在这层访问完后才会访问下一层。

但是问题又出现了:虽然节点访问是按照层进行,但我不知道访问的节点是第几层(如果不给节点增添信息的话)。

错误

1,构造二叉树

代码展示

Node* create_tree(int* arr, int len)
{
	Node* root = new Node(arr[0]);

	for (int i = 1; i < len; i++)
	{
		Node* current = root;
		Node* pre = root;
		while (current)
		{
			pre = current;
			if (arr[i] <= current->value)
			{
				current = current->left;
			}
			else
			{
				current = current->right;
			}
		}

		Node* newNode = new Node(arr[i]);
		if (!pre->left) pre->left = newNode;
		else pre->right = newNode;
	}

	return root;
}

你会发现,我的代码构造的二叉树是会先将所在层填满后再往下一层添加。

修改

Node* newNode = new Node(arr[i]);
if (arr[i] <= pre->value) pre->left = newNode;
else pre->right = newNode;

2,范围循环

3,链表插入

unordered_map<int,Node*> ans;
ans[level].next = newNode;

这么链接节点会丢失节点

所有选择数据结构list

代码

#include<iostream>
#include<queue>
#include<unordered_map>
#include<list>

using namespace std;

struct Node
{
	int value;
	Node* left;
	Node* right;

	Node(int x) : value(x), left(NULL), right(NULL) {};
};



Node* create_tree(int* arr, int len)
{
	Node* root = new Node(arr[0]);

	for (int i = 1; i < len; i++)
	{
		Node* current = root;
		Node* pre = root;
		while (current)
		{
			pre = current;
			if (arr[i] <= current->value)
			{
				current = current->left;
			}
			else
			{
				current = current->right;
			}
		}

		Node* newNode = new Node(arr[i]);
		if (arr[i] <= pre->value) pre->left = newNode;
		else pre->right = newNode;
	}

	return root;
}

class Search
{
private:
	queue<Node*> myqueue;
	unordered_map<int, int> record;
	unordered_map<int, list<int>> ans;

public:

	Search(Node* root)
	{ 
		myqueue.push(root); 
		record[root->value] = 0;
	};

	void search()
	{
		Node* cur;
		
		while (!myqueue.empty())
		{
			cur = myqueue.front();
			myqueue.pop();
			int level = record.find(cur->value)->second;
			record.erase(cur->value);
			ans[level].push_back(cur->value);

			if (cur->left)
			{
				record.insert({ cur->left->value,level + 1 });
				myqueue.push(cur->left);
			}
			if (cur->right)
			{
				record.insert({ cur->right->value,level + 1 });
				myqueue.push(cur->right);
			}
		}
	}

	void print_ans()
	{
		for (const auto& list : ans)
		{
			cout << "the level of " << list.first << " :";
			for (const auto& node : list.second)
			{
				cout << node << " ";
			}
			cout << endl;
		}
	}
};


void preOrder_traverse(Node* root)
{
	if (!root) return;
	cout << root->value << " ";
	preOrder_traverse(root->left);
	preOrder_traverse(root->right);
}


int main()
{
	int arr[] = { 5,1,4,7,3,9,10,6,8,2 };
	int len = sizeof(arr) / sizeof(int);
	Node* root = create_tree(arr, len);

	Search bfs(root);
	bfs.search();
	bfs.print_ans();
	return 0;
}

二,加强版

使用数组来实现队列

class Queue
{
private:
	Node* arr[100] = { 0 };
	int head, tail;
	int remain = 100;
	//head和tail是数组索引,但模拟队列头和尾
public:

	Queue() :head(0), tail(0) {};
	bool empty()
	{
		if (remain == 100) return true;
		else return false;
	}

	Node* front()
	{
		return arr[head];
	}

	void pop()
	{
		head = (head + 1) % 100;
		remain++;
	}

	void push(Node* input)
	{
		if (remain < 1)
		{
			cout << "no room !" << endl;
			return;
		}
		arr[tail] = input;
		tail = (tail + 1) % 100;
		remain--;
	}
};

利用有限的数组设置队列,通过变量remain来判断队列是否满,从而避免头尾相碰。

后面也是直接将queue<Node*> myqueue 改为Queue myqueue就行。

再改进

在已知树节点的个数,这样可以确定数组队列大小。

代码

#include<iostream>
#include<queue>
#include<unordered_map>
#include<list>

using namespace std;

struct Node
{
	int value;
	Node* left;
	Node* right;

	Node(int x) : value(x), left(NULL), right(NULL) {};
};


Node* create_tree(int* arr, int len);


class Search
{
private:
	Node* arr[10];
	unordered_map<int, int> record;
	unordered_map<int, list<int>> ans;

public:
	int head, tail;
	

	void search(Node* root)
	{
		head = tail = 0;
		int level = 0;
		arr[tail++] = root;

		while (head < tail)
		{
			int size = tail - head;
			list<int> newlist;
			//size决定了接下来进行几次操作。
			for (int i = 0; i < size; i++)
			{
				Node* temp = arr[head++];
				newlist.push_back(temp->value);
				//这是将队列里的节点加入到答案节点。

				if (temp->left)arr[tail++] = temp->left;
				if (temp->right)arr[tail++] = temp->right;
				//将队列里的节点的子节点加入队列中。
			}
			//在执行完这个for循环后,该层的所有节点都完成了遍历,
			// 同时下一层的节点全部都加入到队列中去了
			ans.insert({ level++,newlist });

			//为了避免head和tail最后抵达边界
			//就要使用环形队列。
		}
	}

	void print_ans()
	{
		for (const auto& it : ans)
		{
			cout << "Level " << it.first << " :";
			for (const auto& node : it.second)
			{
				cout << node << " ";
			}
			cout << endl;
		}
	}
};



int main()
{
	int arr[] = { 5,1,4,7,3,9,10,6,8,2 };
	int len = sizeof(arr) / sizeof(int);
	Node* root = create_tree(arr, len);

	Search bfs;
	bfs.search(root);
	bfs.print_ans();
	return 0;
}


Node* create_tree(int* arr, int len)
{
	Node* root = new Node(arr[0]);

	for (int i = 1; i < len; i++)
	{
		Node* current = root;
		Node* pre = root;
		while (current)
		{
			pre = current;
			if (arr[i] <= current->value)
			{
				current = current->left;
			}
			else
			{
				current = current->right;
			}
		}

		Node* newNode = new Node(arr[i]);
		if (arr[i] <= pre->value) pre->left = newNode;
		else pre->right = newNode;
	}

	return root;
}

如果害怕数组越界,可以参考前面的使用remain表示剩余空间的方法。

三,二叉树的锯齿形层序遍历

蛇形遍历吧

如果不嫌烦的话,可以像上面的层序遍历一样,只是有了翻转链表的过程。

1,使用数组队列

错误

a,

for (int i = reverse ? tail - 1 : head, int j = reverse ? -1 : 1, int k = 0;
	k < size; i += j, k++)
{
	Node* cur = arr[i];
	newlist.push_back(cur->value);
}

变量声明只需要一个类型标识符。

b,

忘记更新reverse,导致一直从左往右遍历。

代码

void search(Node* root)
{
	bool reverse = false;
	//当reverse为false,代表从左往右遍历;为true,代表从右往左遍历。
	int level = 0;
	head = tail = 0;
	arr[tail++] = root;

	while (head < tail)
	{
		int size = tail - head;
		list<int> newlist;

		//如果是false,就是从左到右,head……tail-1,索引增。
		//如果是true,就是从右到左,tail-1……head,索引减。
		for (int i = reverse ? tail - 1 : head, j = reverse ? -1 : 1, k = 0;
			k < size; i += j, k++)
		{
			Node* cur = arr[i];
			newlist.push_back(cur->value);
		}
		//完成了该层节点的遍历。

		//下面是为下次宽度遍历做准备,由size决定操作次数,
		// 要将多少个子节点加入到队列中
		for (int i = 0; i < size; i++)
		{
			Node* temp = arr[head++];

			if (temp->left)arr[tail++] = temp->left;
			if (temp->right)arr[tail++] = temp->right;
			//将队列里的节点的子节点加入队列中。
		}
		//在执行完这个for循环后,该层的所有节点都都加入到队列中去了
		ans.insert({ level++,newlist });

		//一定不能忘了更新reverse,我第一次就是忘了导致输出是基础的bfs
		reverse = !reverse;
	}
}

2,使用双向队列

首先看看错误代码

我是看AI给的代码改的,傻B的AI,真不能全信,关键时刻还得靠自己。

void search(Node* root)
{
    deque<Node*> queue;
    bool reverse = true;
    int level = 0;
    queue.push_back(root);
    int size = queue.size();

    while (!queue.empty())
    {
        list<int> newlist;
        for (int i = 0; i < size; i++)
        {
            Node* cur;
            if (reverse)
            {
                cur = queue.front();
                queue.pop_front();
                newlist.push_back(cur->value);
            }
            else
            {
                cur = queue.back();
                queue.pop_back();
                newlist.push_back(cur->value);
            }
            
            if (cur->left)queue.push_back(cur->left);
            if (cur->right)queue.push_back(cur->right);
        }
        
        size = queue.size();
        reverse = !reverse;
        ans[level++] = newlist;
    }
}

错误点在后面插入子节点时,都采用后序插入,如果是从头部访问弹出无影响,但是从尾部就有问题了。

我想了下关键是遍历和加入下层节点两个过程不能冲突,

我想了个办法。也是对上面问题思考,设计算法,一定要像写数学题一样打草稿,不断地试错改进。

解决办法

对于双向队列:

从头访问,从尾加入,先加左后加右子节点;

从尾访问,从头加入,先加右后加左子节点。

代码

void search(Node* root)
{
    deque<Node*> queue;
    bool reverse = true;
    int level = 0;
    queue.push_back(root);
    int size = queue.size();

    while (!queue.empty())
    {
        list<int> newlist;
        for (int i = 0; i < size; i++)
        {
            Node* cur;
            if (reverse)
            {
                cur = queue.front();
                queue.pop_front();
                newlist.push_back(cur->value);
                if (cur->left)queue.push_back(cur->left);
                if (cur->right)queue.push_back(cur->right);
            }
            else
            {
                cur = queue.back();
                queue.pop_back();
                newlist.push_back(cur->value);
                if (cur->right)queue.push_front(cur->right);
                if (cur->left)queue.push_front(cur->left);
            }
            
            
        }
        
        size = queue.size();
        reverse = !reverse;
        ans[level++] = newlist;
    }
}

使用数组方法的话就不用考虑冲突问题,因为使用了临时变量i来访问数组。

四,二叉树的最大的特殊宽度

算每层的宽度,一般都是不考虑空节点。如果考虑空节点,求宽度。

每层计算的起始是从第一个不为空的节点到最后一个不为空的节点。

首先我们得了解到之前使用数组实现堆的时候就是利用编号(索引)来找父节点和子节点。

同样我们不需要管哪些空节点,只需要知道节点的编号就可以了。当然我们不可能再用数组来表示树了。所以结构体肯定要多一项属性。

前置知识:

已知该节点的编号是i,则左子节点为2 * i,右为2 * i + 1。

父节点直接就是i / 2。

如果结构体可以自定义话可以用上面方法,但只是单纯给你传入一个树的头节点,你又该怎么办?

步骤

1,遍历每一层,找到最左和最右的节点。

2,准备两个队列,一个队列放节点,一个放节点对应的编号。

(我觉的话也可以用哈希map,如果觉得队列很麻烦的话)

代码

本来自己代码是正确的,我宁愿怀疑AI是错的,后没有怀疑一下我自己,二叉树的图画错了。

#include<iostream>
#include<queue>
#include<list>
#include<unordered_map>

using namespace std;

struct Node
{
    int value;
    Node* left;
    Node* right;

    Node(int x) : value(x), left(NULL), right(NULL) {};
};

Node* create_tree(int* arr, int len);

class Search
{
private:
    queue<Node*> myqueue;
    queue<int> number;
    unordered_map<int, int> ans;
public:
    int level = 0;
    int width = 1;
    
    void traverse(Node* root)
    {
        myqueue.push(root);
        number.push(1);
        while (!myqueue.empty())
        {
            int left = number.front();
            int right = number.back();
            width = right - left + 1;
            ans[level++] = width;

            for (int i = 0; i < myqueue.size(); i++)
            {
                Node* cur = myqueue.front();
                int num = number.front(); // 保存当前节点的序号
                myqueue.pop();
                number.pop();
                if (cur->left) 
                {
                    myqueue.push(cur->left);
                    number.push(2 * num);
                }
                if (cur->right) 
                {
                    myqueue.push(cur->right);
                    number.push(2 * num + 1);
                }
            }
        }
    }

    void print_ans()
    {
        for (const auto& it : ans)
        {
            cout << "Level " << it.first << " :" << it.second << endl;
        }
    }
};

int main()
{
    int arr[] = { 5,1,4,7,3,9,10,6,8,2 };
    int len = sizeof(arr) / sizeof(int);
    Node* root = create_tree(arr, len);

    Search bfs;
    bfs.traverse(root);
    bfs.print_ans();
    return 0;
}


Node* create_tree(int* arr, int len)
{
    Node* root = new Node(arr[0]);

    for (int i = 1; i < len; i++)
    {
        Node* current = root;
        Node* pre = root;
        while (current)
        {
            pre = current;
            if (arr[i] <= current->value)
            {
                current = current->left;
            }
            else
            {
                current = current->right;
            }
        }

        Node* newNode = new Node(arr[i]);
        if (arr[i] <= pre->value) pre->left = newNode;
        else pre->right = newNode;
    }

    return root;
}

当然可以使用数组队列去实现。

五,二叉树的最大深度和最小深度

1,最大深度

原理:使用递归函数,先求左子树的高度,再求右子树的高度,然后左右中大的那一个高度加上1,因为要算上自己这层高度。

代码

int max_depth(Node* root)
{
    if (!root) return 0;
    else
    {
        int a = max_depth(root->left);
        int b = max_depth(root->right);
        return a > b ? a + 1 : b + 1;
    }

2,最小深度

算高度时一定是到叶节点才算结束。必须搜索到方框的数字才截至。

原理:

算左右两边的最小高度,然后取他们中最小的再加1。

注意基准情况。如果为空节点直接返回0,如果左右子节点都为空,直接返回1.

但同时也要注意如果某个节点的左或右节点为空,但另一个子节点存在,就不能最小子树高度为0.

所以开始设置为一个很大的数。

代码

int min_depth(Node* root)
{
    if (!root) return 0;
    //当前树是空树时,直接返回0
    if (!root->left && !root->right) return 1;
    //叶节点直接返回1

    int ldepth = Max;
    int rdepth = Max;
    //当某个节点只有一个子节点时,不能直接调用空节点。所以才有让l,r初始化
    if (root->left) ldepth = min_depth(root->left);
    if (root->right) rdepth = min_depth(root->right);

    return ldepth < rdepth ? ldepth + 1 : rdepth + 1;
}

六,二叉树的先序序列化和反序序列化

1,定义,概念

序列化(Serialization)是指将数据结构或对象转换为一种连续的字节流(byte stream)的过程,这样可以方便地存储或传输数据。反序列化(Deserialization)则是指将字节流转换回原始的数据结构或对象。

在二叉树的序列化中,我们需要将二叉树的结构和节点值转换为一种连续的字节流。二叉树的序列化通常使用以下两种方法:

  1. 前序遍历序列化(Preorder Traversal):先访问根节点,然后递归地访问左子树和右子树。这种方法可以得到一个前序遍历的序列,如:root -> left -> right
  2. 层序遍历序列化(Level Order Traversal):从根节点开始,逐层访问节点,先访问父节点,然后访问子节点。这种方法可以得到一个层序遍历的序列,如:root -> left -> right -> left-left -> left-right -> right-left -> right-right

二叉树的序列化可以使用以下步骤:

  1. 选择序列化方法:选择前序遍历或层序遍历作为序列化方法。
  2. 遍历二叉树:使用选择的序列化方法遍历二叉树,访问每个节点并记录其值和子节点指针。
  3. 构造序列化字符串:将访问的节点值和子节点指针转换为一个字符串,使用特定的分隔符(如逗号或空格)分隔每个节点的信息。
  4. 存储或传输序列化字符串:将序列化字符串存储在文件中或通过网络传输给其他程序。

反序列化二叉树的过程与序列化相反,我们需要将序列化字符串转换回原始的二叉树结构。这个过程通常涉及以下步骤:

  1. 读取序列化字符串:从文件或网络中读取序列化字符串。
  2. 解析序列化字符串:使用特定的分隔符分隔每个节点的信息,并解析出节点值和子节点指针。
  3. 构造二叉树:使用解析出的节点值和子节点指针构造原始的二叉树结构。

二叉树的序列化和反序列化在很多应用中非常重要,例如:

  • 数据存储:序列化二叉树可以将其存储在文件或数据库中,以便后续使用。
  • 数据传输:序列化二叉树可以将其传输给其他程序或系统,以便共享或协同工作。
  • 算法设计:序列化二叉树可以帮助设计更高效的算法,例如二叉树的查找、插入和删除操作。

2,实现

序列化和反序列化要使用同一套规则。

如果是前序遍历

不同结构的树有不同的字符串表示

如果使用中序遍历,就没有一一对应。

3,补充

高效追加字符串

4,序列化

代码

string serialize(Node* root)
{
    string ans;
    stringstream* str = new stringstream;
    recursion(root, str);
    *str >> ans;
    return ans;

}

void recursion(Node* root, stringstream* str)
{
    //利用先序遍历,第一次到达该节点时进行操作,即序化。
    if (!root)//如果当前节点为空,就用#来表示。
    {
        *str << "#,";
    }
    else
    {
        *str << root->value << ",";
        recursion(root->left,str);
        recursion(root->right, str);
    }
}

结果

改进

5,反序列化

补充

如何按指定的字符分割字符串。

1)

#include <iostream>
#include <sstream>
#include <vector>
#include <string>

int main() {
    std::string str = "5,1,#,4,3,2,#,#,#,#,7,6,#,#,9,8,#,#,10,#,#";
    std::stringstream ss(str);
    std::string token;
    std::vector<std::string> tokens;
    ss << str;

    while (std::getline(ss, token, ',')) {
        tokens.push_back(token);
    }

    for (const auto& token : tokens) {
        std::cout << token << std::endl;
    }

    return 0;
}

2)

#include<iostream>
#include<vector>
#include<string>
#include<sstream>

using namespace std;

vector<string> split(string& str, char ch);

int main()
{
	stringstream ss;
	string str = "5,1,#,4,3,2,#,#,#,#,7,6,#,#,9,8,#,#,10,#,#";
	string token;
	vector<string> tokens;
	tokens = split(str, ',');
	for (string& it : tokens)
	{
		cout << it << " ";
	}
}

vector<string> split(string& str, char ch)
{
	vector<string> vec;
	int pos = 0, pre = 0;
	do
	{
		pos = str.find(ch,pre);
		if (pos == string::npos) pos = str.length();
		string token = str.substr(pre, pos - pre);
		if (!token.empty()) vec.push_back(token);
		pre = pos + 1;
	} while (pre < str.length());

	return vec;
}

代码

Node* deserialize(string data)
{
    stringstream ss;
    vector<string> tokens;
    string token;
    ss << data;
    while (getline(ss, token, ','))
    {
        tokens.push_back(token);
    }

    cnt = 0;
    return recursion2(tokens);
}

int cnt;
//设置成全局变量,记录当前数组消费到了哪里。
//在其他地方都能影响到cnt,实现向量遍历。
Node* recursion2(vector<string> strs)
{
    if (cnt >= strs.size()) return NULL;
    string cur = strs[cnt++];
    if (cur == "#") return NULL;
    else
    {
        Node* treenode = new Node(stoi(cur));
        //然后去消费左右子树
        treenode->left = recursion2(strs);
        //因为recursion2是有返回值的,所以成功将节点连接,树得以搭建。
        treenode->right = recursion2(strs);
        return treenode;
    }
}

有时使用全局变量就可以避免给函数参数,直接在函数内部修改全局变量。

完整代码

#include<iostream>
#include<string>
#include<sstream>
#include<vector>
using namespace std;

struct Node
{
    int value;
    Node* left;
    Node* right;

    Node(int x) : value(x), left(NULL), right(NULL) {};
};

Node* create_tree(int* arr, int len);
void traverse(Node* root);

class Search
{
public:
    
    string serialize(Node* root)
    {
        string ans;
        stringstream* str = new stringstream;
        recursion1(root, str);
        *str >> ans;
        return ans;

    }

    void recursion1(Node* root, stringstream* str)
    {
        //利用先序遍历,第一次到达该节点时进行操作,即序化。
        if (!root)//如果当前节点为空,就用#来表示。
        {
            *str << "#,";
        }
        else
        {
            *str << root->value << ",";
            recursion1(root->left,str);
            recursion1(root->right, str);
        }
    }

    Node* deserialize(string data)
    {
        stringstream ss;
        vector<string> tokens;
        string token;
        ss << data;
        while (getline(ss, token, ','))
        {
            tokens.push_back(token);
        }

        cnt = 0;
        return recursion2(tokens);
    }

    int cnt;
    //设置成全局变量,记录当前数组消费到了哪里。
    //在其他地方都能影响到cnt,实现向量遍历。
    Node* recursion2(vector<string> strs)
    {
        if (cnt >= strs.size()) return NULL;
        string cur = strs[cnt++];
        if (cur == "#") return NULL;
        else
        {
            Node* treenode = new Node(stoi(cur));
            //然后去消费左右子树
            treenode->left = recursion2(strs);
            //因为recursion2是有返回值的,所以成功将节点连接,树得以搭建。
            treenode->right = recursion2(strs);
            return treenode;
        }
    }

    
};

int main()
{
    int arr[] = { 5,1,4,7,3,9,10,6,8,2 };
    int len = sizeof(arr) / sizeof(int);
    Node* root = create_tree(arr, len);

    Search bfs;
    string str = bfs.serialize(root);
    cout << str << endl;

    Node* newroot = bfs.deserialize(str);
    traverse(newroot);
    
    return 0;
}


Node* create_tree(int* arr, int len)
{
    Node* root = new Node(arr[0]);

    for (int i = 1; i < len; i++)
    {
        Node* current = root;
        Node* pre = root;
        while (current)
        {
            pre = current;
            if (arr[i] <= current->value)
            {
                current = current->left;
            }
            else
            {
                current = current->right;
            }
        }

        Node* newNode = new Node(arr[i]);
        if (arr[i] <= pre->value) pre->left = newNode;
        else pre->right = newNode;
    }

    return root;
}


void traverse(Node* root)
{
    if (!root)return;
    cout << root->value << " ";
    traverse(root->left);
    traverse(root->right);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值