“程序(Program)=数据结构(Data Structure)+算法(Algorithm)”
抽象数据类型:
定义:用程序定义的一个(数学)模型以及定义在该模型上的一组操作。
什么是数据:
数据
数据(Data)是信息的载体,是可以被计算机识别,存储并加工处理的描述客观事物的信息符号的总称。数据不仅仅包括了整形,浮点数等数值类型,还包括了字符甚至声音,视频,图像等非数值的类型。
什么是数据结构:
数据结构(Data Structures)主要是指数据和数据之间关系的集合,数据指的是计算机中需要处理的数据,而关系指的是这些数据相关的前后逻辑,
数据结构中的四种逻辑结构:
2. 四大逻辑结构(Logic Structure)
1) 集合结构
集合结构(Set Structure)中所有数据元素除了同属于一个集合外,并无其他关系。
如图:
2) 线性结构
线性结构(Linear Structure)指的是数据元素之间存在“一对一的关系”
如图:
3) 树形结构
树形结构(Tree Structure)指的是数据元素之间存在“一对多”的层次关系。
如图:
4) 图形结构
图形结构(Graphic Structure,也称:网状结构)指的是数据元素之间存在“多对多的关系”(注:此时的“多对多”中的多表示,至少有一个)
图示:
来源----链接
数据结构:
线性表:
零个或者多个数据元素的有限序列
注意,不是数组,也不是链表,是序列。
线性表的特性:
1,同一个线性表中的元素必须是同一类数据元素
2,必须有限,前无前驱,后无后继
3,必须是线性,没有分支
好处:
线性表的实现种类:
用连续内存来存储数据元素的:顺序表。
用
数组:
特点,数据连续存储
好处:可以随机访问
坏处:内存不可以动态扩展,数组最大的缺点就是我们的插入和删除时需要移动大量的元素,显然这需要消耗大量的时间。
数组的弊端有二
其一:所需要移动的元素很多,浪费算力(移动大,耗费多)。
其二:必须为数组开足够多的空间,否则有溢出风险(空间大小有限制)。
链表:
特点:利用结构体形成结点,数据不一定连续存储
静态链表:在栈上生成
动态链表:在堆上生成
栈和堆:
都是用户空间的内存区
单链表:
缺点:
只能单方向进行数据操作
双向链表:
双向链表可以简称为双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
循环链表:
栈:
叠盘子原理:先进后出
当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,这时我们就应该首选“栈”这种数据结构
栈:
栈的生长方向
栈主要包含两个操作,入栈和出栈,
如何实现应该栈:
栈既可以用数组来实现,也可以用链表来实现。用数组实现的栈,我们叫作顺序栈,用链表实现的栈,我们叫作链式栈。
实现:
1,顺序栈
顺序栈是一种更为快速的实现栈的方法
其可以快速的帮助我们构建代码,分析过程,相应的实现起来也更加的便捷。
用数组来实现:
如果用数组来实现,那么一个数组元素就是栈的一个结点,数组的最后一个元素就是栈顶(先入后出)
如果用数组实现栈,那么对栈的操作的函数(入栈,出栈等都需要程序员自己定义)
用容器来实现:vector
这种方式是最快实现栈的方法,因为所有的操作都定义好了,不需要程序员自己再定义了。
#include <iostream>
class MyStack {
private:
vector<int> data; // store elements
public:
/** Insert an element into the stack. */
void push(int x) {
data.push_back(x);
}
/** Checks whether the queue is empty or not. */
bool isEmpty() {
return data.empty();
}
/** Get the top item from the queue. */
int top() {
return data.back();
}
/** Delete an element from the queue. Return true if the operation is successful. */
bool pop() {
if (isEmpty()) {
return false;
}
data.pop_back();
return true;
}
};
int main() {
MyStack s;
s.push(1);
s.push(2);
s.push(3);
for (int i = 0; i < 4; ++i) {
if (!s.isEmpty()) {
cout << s.top() << endl;
}
cout << (s.pop() ? "true" : "false") << endl;
}
}
2,链式栈
1,建立一个结构体Node表示结点,其中包含有一个data域和next指针。
next指针表示,下一个的指针,其指向下一个结点,通过next指针将各个结点连接起来,让所有栈元素相互联系。
2,再设计一个结构体,其包括了一个永远指向栈头的Node类型的指针top和一个计数器count记录元素个数,(也可以设计成一个指针top和一个指针bottom分别指向栈头和栈尾)其主要功效就是设定允许操作元素的指针以及确定栈何时为空(count的方法是当count为0时为空,top和bottom方法就是两者指向同一个空间时为栈为空)
入栈
入栈(push)操作时,我们只需要找到top所指向的空间,创建一个新的结点,将新的结点的next指针指向我们的top指针指向的空间,再将top指针转移,指向新的结点,即是入栈操作
出栈
出栈就是删除这个结点,即free或者delete这个结构体变量
free
(temp);
p->count--;
从栈低开始让数据入栈(从栈底开始存储数据)
从栈顶开始让数据出栈(从栈顶开始读出数据)
(栈只是一种规定了的实现数据操作方式的方法,并没有像数据类型一样定义出来)
《网络编程中所运用到的小端字节序和大端字节序,说的就是数据在栈中的存储方式:
0x12341244(44是低位,12是高位)
11010101011(后面是低位,前面是高位)
小端字节序:(高高低低)
高位存在高地址,低位存在低地址
(电脑上一般是小端字节序)
大端字节序:
高位存在低地址,低位存在高地址
(网络服务器用大端字节序,这就是为什么网络编程时,需要把地址进行转换,因为网络协议使用大端字节序来传输数据)》
队列
队列是一个先进先出的数据结构。
栈是不通低的,而队列是通低的
队列是一个线性的数据结构,规定这个数据结构只允许在一端进行插入(队尾),另一端进行删除(队头),禁止直接访问除这两端以外的一切数据。
从屁股吃入,从嘴巴吐出
队列的创建
创建一个结点结构体node,再创建一个queue结构体包含两个结点结构体的指针变量,分别指向队列的队尾和队头
typedef struct node{
int data;
struct node *next;
}node;
//队列定义,队首指针和队尾指针
typedef struct queue{
node *front; //头指针
node *rear; //尾指针
}queue;
判断队列是否为空:
因为队列是顺序插入的,一个挤一个往后排的,而且只能从队尾删除,所以如果第一个结点为空,则后面的都空,所以判断队列是否为空,只需要判断第一个结点是否为空即可
if(queue->front==NULL) 则队列为空
入队(push)
进行入队(push)操作的时候,我们首先需要特判一下队列是否为空(即队列还没有建立,queue的front,rear还是空的),如果队列为空的话,需要将头指针和尾指针一同指向刚刚建立的这个结点,即front=n;rear=n
如果队列不空,因为front一直指向第一个结点,所以,front不动,只需要将之前的最后一个结点(rear指向的结点)的next指向刚刚建立的结点,再让rear指向刚刚建立的这个结点即可
void push(queue *q,int data){
node *n =init_node();
n->data=data;
n->next=NULL; //采用尾插入法
//if(q->rear==NULL){ //使用此方法也可以
if(empty(q)){
q->front=n;
q->rear=n;
}else{
q->rear->next=n; //n成为当前尾结点的下一结点
q->rear=n; //让尾指针指向n
}
}
出队(pop)
同样,,出队也需要判断队列是否为空
再判断是否只有一个结点,如果只有一个结点,出队之后,front和rear就要为NULL
如果不只一个结点,那么rear不变,front指向前一个结点
void pop(queue *q){
node *n=q->front;
if(empty(q)){
return ; //此时队列为空,直接返回函数结束
}
if(q->front==q->rear){
q->front=NULL; //只有一个元素时直接将两端指向制空即可
q->rear=NULL;
free(n); //记得归还内存空间
}else{
q->front=q->front->next;
free(n);
}
}
遍历队列元素
让一个指针循环指向结点,并打印元素即可
循环队列
为什么需要循环队列:
内存的开辟是不断向后开辟内存空间的,如果我们需要对我们的队列进行频繁的入队和出队操作,那么可能会达到系统预留给程序的内存上界,而使得前面还有内存可以用,但是后面的内存已经达到边界,使得数据入队无处可存(假溢出)
循环队列的思维非常简单,就是给定我们队列的大小范围,在原有队列的基础上,只要队列的后方(队尾)满了,就从这个队列的前面(队头)开始进行插入,以达到重复利用空间的效果,由于循环队列的设计思维更像一个环,因此常使用一个环图来表示,但注意其不是一个真正的环,循环队列依旧是单线性的
循环队列的创建:
将尾结点的next指针指向头结点即可
循环队列的遍历:
遍历结束的标志是结点的nex指向头结点
双向循环列表:
尾指向头,头指向尾
数组,链表,栈,队列的比较和应用
内存 | 优点 | 缺点 | 实现方式 | 应用场景 | 插入,删除方式 | |
数组 | 连续 | 可随机访问 | 不可动态扩展, 改动耗费大 | 可随处插入,删除 | ||
链表 | 不一定 | 可动态扩展,改动方便 | 查询麻烦 | 静态链表:栈上生成 动态链表:堆上生成 单链表,双向链表,循环链表 | 可随处插入,删除 | |
栈 | 数组实现:连续 链表实现:不一定 | 只能对一端进行操作 | 顺序栈:数组实现 链式栈:链表实现 | 栈顶插入,栈顶删除 | ||
队列 | 链表实现:不一定 | 只能一端添加,一段获取 | 链表 | 屁股进,嘴巴出 |
堆和栈
数据结构 | 内存 | 进程分配 | |
堆 | 树形 | 栈中的数据占用的内存空间的大小是确定的,便于代码执行时的入栈、出栈操作,并由系统自动分配和自动释放内存可以及时得到回收,相对于堆来说,更加容易管理内存空间。 | 一个进程一个堆(进程独用) |
栈 | 线性 | 堆是动态分配内存,内存大小不一,也不会自动释放。栈中的数据长度不定,且占空间比较大。便于开辟内存空间,更加方便存储。 | 一个线程一个栈(进程共享) |
树
属于非线性数据结构结构的一种,
树的基本术语
l 节点的度:树中某个节点的子树的个数。
l 树的度:树中各节点的度的最大值。
l 分支节点:度不为零的节点。
l 叶子节点:度为零的节点。
l 路径:i->j;路径长度:路径经过节点数目减1。
l 孩子节点:某节点的后继节点;双亲节点:该节点为其孩子节点的双亲节点(父母节点);兄弟节点:同一双亲的孩子节点;子孙节点:某节点所有子树中的节点;祖先节点:从树节点到该节点的路径上的节点。
l 节点的层次:根节点为第一层(以此类推);树的高度:树中节点的最大层次。
l 有序树:树中节点子树按次序从左向右安排,次序不能改变;无序树:与之相反
l 森林:互不相交的树的集合。
高度看为楼层,深度看为水井,两者都有0
树的性值
l 树的节点树为所有节点度数加1(加根节点)。
l 度为m的树中第i层最多有m^(i-1)个节点。
l 高度为h的m次树至多(m^h-1)/(m-1)个节点。
l 具有n个节点的m次树的最小高度为logm( n(m-1) + 1 ) 向上取整。
特点:
1,注意:每个节点的子节点不一定是两个
重点--二叉树:
特点:
每个结点中最多拥有一个左结点和一个右结点
也可以只拥有一个
1)每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点。
2)左子树和右子树是有顺序的,次序不能任意颠倒。
3)即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
二叉树特征
性质1:二叉树第i层上的结点数目最多为 2的(i-1)次方个节点(i≥1)。
性质2:深度为k的二叉树至多有2的k次方-1个结点(k≥1)。
性质3:包含n个结点的二叉树的高度至少为log2 (n+1)。
性质4:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1。
几种特殊的二叉树
斜树
斜树:所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树
满二叉树
满二叉树:在一棵二叉树中。如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
满二叉树的特点有:
1)叶子只能出现在最下一层。出现在其它层就不可能达成平衡。
2)非叶子结点的度一定是2。
3)在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。
完全二叉树
完全二叉树:
叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫做完全二叉树。
特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。
这里说的 “最后一层的叶子节点都靠左排列”不是最后一层的子节点是左节点,而是指最后一层的子节点,从 左数到右是连续,中间没有断开,缺少节点。由基于数组的顺序存储法,可以知道完全二叉树是不会浪费内存的。其实简单理解,完全是为了省内存而提出这样的概念
满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树
二叉树
一种每一个结点中只允许拥有左右孩子(或为空)的树
结点设计
一颗二叉树的结点设计一定要有如下内容:
a)结点元素,data域,用来存储数据,其可以是int,char等基本的类型,同时也可以是struct等这些复杂的复合数据类型。
b)左孩子结点,left指针,总是用来指向当前结点的下一层的左边结点,其属于一种指针。
c)右孩子结点,right指针,总是用来指向当前结点的下一层的右边结点,其属于一种指针。
d)父结点(可选),parent指针,总是指向当前结点的前一个结点,简称父亲结点,其不属于必须结点设计,省略掉可以打扰节省内存的效果,而使用则可以更方便进行定向搜索,本案例中不使用父节点。
以上就是一颗二叉树的结点设计,除此之外,我们使用一棵树的时候需要建立一颗树根,由这个“根”,来进行逐步的向下构建。
其设计代码可以表示为:
//树的结点
typedef struct node{
int data;
struct node* left;
struct node* right;
} Node;
//树根
typedef struct {
Node* root;
} Tree;
二叉树的创建
二叉树的建立不是编程重点,而二叉树是作为STL一些容器的底层数据结构,理解和使用才是重点
#include "iostream"
using namespace std;
struct node
{
char c;
node* left=NULL;
node* right=NULL;
};
node* create()
{
node* nd=new node;
//char ch=getchar();
char ch;
scanf("%c",&ch);
getchar();
if(ch=='#')
nd=NULL;
else
{
nd->c=ch;
nd->left=create();
nd->right=create();
}
return nd;
}
void display(node* nd)
{
if(nd!=NULL)
{
printf("%c\n",nd->c);
display(nd->left);
display(nd->right);
}
}
int main()
{
node* root=create();
display(root);
return 0;
}
注意:如果scanf之后没有getchar进行过滤,那么下一次再读取时将读到'\n',导致程序出错
下面同理:
node* nd=new node;
char ch=getchar();
//char ch;
//scanf("%c",&ch);
getchar();
if(ch=='#')
nd=NULL;
二叉树创建过程解析:
方法:递归
层层递归进入,再返回每一层的根结点
注意:返回时,最后一层的左右结点都必须为空才能返回
赋值时不好理解,层层画图理解
递归中的注意事项:
只要第一个递归函数没有退出,那么第二个函数永远不会被执行。
二叉树的遍历
先序遍历:根左右
中序遍历:左根右
后序遍历:左右根
先中后指的是根的数据在先/中/后被访问
实际上,二叉树的前、中、后序遍历就是一个递归的过程。比如,前序遍历,其实就是先打印根节点,然后再递归地打印左子树,最后递归地打印右子树
1,先序遍历:根左右
//树的先序遍历 Preorder traversal
void preorder(Node* node){
if (node != NULL)
{
printf("%d ",node->data);
inorder(node->left);
inorder(node->right);
}
}
inorder(node->left);//这部分代码会一直递归到退出,程序才会继续执行下面的
inorder(node->right);
2,中序遍历:左根右
//树的中序遍历 In-order traversal
void inorder(Node* node){
if (node != NULL)
{
inorder(node->left);
printf("%d ",node->data);
inorder(node->right);
}
}
3,后序遍历:左右根
//树的后序遍历 Post-order traversal
void postorder(Node* node){
if (node != NULL)
{
inorder(node->left);
inorder(node->right);
printf("%d ",node->data);
}
}
二叉树的存储方式
1,基于指针或者引用的链式存储
2,基于数组的顺序存储
详情---链接
二叉查找树(二叉搜索树)
二叉查找树是为了实现快速查找而生的。不过,它不仅仅支持快速查找一个数据,还支持快速插入、删除一个数据。
二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。
二叉查找树的查找,插入,删除操作
二叉查找树的高度怎么计算
递归法,根节点高度=max(左子树高度,右子树高度)+1
代码实现
#include<vector>
#include<iostream>
using namespace std;
class SerchTree{
private:
TreeNode* root;
public:
SerchTree();
//插入节点
void Insert_Node(TreeNode* root,int val){
if(NULL == root)
root = new TreeNode(val);
else{
if(val<root->val)
Insert_Node(root->left,val);
else{ //一样大就往左走吧
Insert_Node(root->right,val);
}
}
}
//从数组中构造二叉搜索树
void Create_SerchTree(vector<int>& vec){
int sz = vec.size();
for(int i = 0;i<sz;i++){
Insert_Node(root,vec[i]);
}
}
//搜索某个节点是否存在
bool SerchNode(TreeNode* root,int val){
if(NULL == root)
return false;
if(val<root->val)
return SerchNode(root->left,val);
else if(val>root->val)
return SerchNode(root->right)
else
return ture;
}
//删除节点
void DelNode(TreeNode* node){
TreeNode* temp;
if(NULL == node->right){ //如果右子节点为空
temp = node;
node = node->left;
delete temp;
}
else{ //如果右子节点不空
temp = node;
while(NULL != temp->left){
temp = temp->left;
}
node->val = temp->val;
delete temp;
}
}
//删除某个节点
void DelSerchNode(TreeNode* root,int val){
if(NULL == root)
return;
if(val<root->val)
return DelSerchNode(root->left,val);
else if(val>root->val)
return DelSerchNode(root->right)
else
DelNode(root);
}
//计算二叉树的最大深度
int maxDepth(Node x) { //1.如果根结点为空,则最大深度为0;
if (x == null)
return 0;
int max = 0;
int maxL = 0;
int maxR = 0;
//2.计算左子树的最大深度;
if (x.left != null)
maxL = maxDepth(x.left);
//3.计算右子树的最大深度;
if (x.right != null)
maxR = maxDepth(x.right);
//4.当前树的最大深度=左子树的最大深度和右子树的最大深度中的较大者+1
max = maxL > maxR ? maxL + 1 : maxR + 1; return max;
}
}
平衡二叉树(AVL)
- 全称为平衡二叉搜索树
- 它是由苏联数学家Adelson-Velsky 和 Landis提出来的,因此平衡二叉树又叫AVL树
什么是平衡二叉树
任意一个节点的左右子树的高度相差不能大于 1的二叉树。
平衡二叉树中的平衡到底指什么
平衡--------二叉树左右子树(基本)对称
高度相差不大于1
平衡二叉查找树中“平衡”的意思,其实就是让整棵树左右看起来比较“对称”、比较“平衡”,不要出现左子树很高、右子树很矮的情况。这样就能让整棵树的高度相对来说低一些,相应的插入、删除、查找等操作的效率高一些。
为什么需要平衡二叉树
二分查找树在插入,删除改动时很容易失衡,即只有左子树或者只有右子树,这会导致时间复杂度变大,查找的效率降低
发明平衡二叉查找树的目的是防止二叉查找树在频繁的插入和删除的情况下,二叉查找树可能退化成链表,时间复杂度由 O(log2n)变为 O(n),从而导致效率下降等一些问题的出现。
红黑树(RB_tree)
什么是红黑树
红黑树是平衡二叉树中规则比较少(不太平衡)的一种
红黑树中的节点,一类被标记为黑色,一类被标记为红色
不太平衡------它的左右子树高差有可能大于 1----》牺牲平衡性换取稳定性
为什么需要红黑树
1,AVL的左右子树高度差不能超过1,每次进行插入/删除操作时,几乎都需要通过旋转操作保持平衡
2,在频繁进行插入/删除的场景中,频繁的旋转操作使得AVL的性能大打折扣
3,红黑树通过牺牲严格的平衡,换取插入/删除时少量的旋转操作,整体性能优于AVL
4,红黑树插入时的不平衡,不超过两次旋转就可以解决;删除时的不平衡,不超过三次旋转就能解决
5,红黑树的红黑规则,保证最坏的情况下,也能保持时间复杂度为O ( l o g 2 N ) 。
总结:
1:牺牲平衡性换取稳定性
2:红黑规则保证效率(时间复杂度)
红黑树的插入和删除改动需要维护的原因:
往平衡二叉树中添加节点很可能会导致二叉树失去平衡(不再满足平衡条件),所以我们需要在每次插入节点后进行平衡的维护操作(旋转和变色)
红黑树的规则
根节点是黑色的;
每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存储数据;
每个红色结点的子结点必须为都是黑色结点(同一层的结点不要求同色,而且可以 出现连续黑色结点)
每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点;
红黑树也是平衡二叉查找树,所以也有左小右大的规则
优点:稳定性好,性能高 (查找效率---O(log2n))
缺点:维护难
红黑树的创建(插入)
红黑树结点中应该具有的属性:
1,左右结点
2,表示结点颜色的变量
注意:用tru和false表示red和black
3,结点中存储的数据
typedef struct rb_node
{
int key;//这个值是查找树比较值
bool color;
rb_node* lchild;
rb_node* rchild;
}RBT*;
同二叉树一样,需要定义一个结构体指向根结点
typedef struct rb_tree
{
rb_node *root=NULL;
}RT*;
插入的注意事项:
1:所有的插入节点,都必须是红节点,除了根结点
2:红黑树是一种查找树,那么在节点的left,都是小于节点的;在节点的right,都是大于节点的。
3:红黑树如何调整平衡:旋转+变色
4:两种情况不需要改动,一是插入根结点,根结点为黑色,二是如果插入节点的父节点是黑色的,那我们什么都不用做,它仍然满足红黑树的定义
如果我们插入的结点不是插入根结点,或者其父结点不是黑色,才需要进行复杂的旋转和变色
旋转+变色
变色:
CASE 1:
如果关注结点是 a,它的叔叔节点 d 是红色,我们就依次执行下面的操作:
将关注结点 a 的父节点 b、叔叔节点 d 的颜色都设置成黑色;
将关注结点 a 的祖父节点 c 的颜色设置成红色;
关注结点变成 a 的祖父节点 c;
跳到 CASE 2 或者 CASE 3。
(右节点)旋转
CASE 2:
如果关注结点是 a,它的叔叔节点 d是黑色,a 是其父节点 b 的右子节点,我们就依次执行下面的操作:
关注节点变成节点 a 的父节点 b;
围绕新的关注节点b 左旋;(右子左旋)
跳到 CASE 3
(左节点)变色+旋转
CASE 3:
如果关注节点是 a,它的叔叔节点 d 是黑色,关注节点 a 是其父节点 b 的左子节点,我们就依次执行下面的操作:(左子右旋)
围绕关注节点 a 的祖父节点 c 右旋;
将关注节点 a 的父节点 b、兄弟节点 c 的颜色互换。
调整结束。
树的构造都是由插入来构造的
找插入结点
什么样的结点是插入结点:空的,位置合适的(大小)空结点
要插入的结点记为A,遍历查询的结点记为B
1,先查询根结点是否为空,为空则直接赋给根结点
2,如果要存入的值比B结点的值大,同时B结点的右结点为空-----则建立A结点,赋值给A再插入为B的右结点,如果非空,向下遍历--看代码
如果要存入的值比B结点的值小,同时B结点的左结点为空-----则建立A结点,赋值给A,再插入为B的左结点,如果非空,向下遍历--看代码
void insert(int data) {
if (tree == null) {//判断根结点是否为空
tree = new Node(data);//如果为空直接赋给根节点
tree->color=0;//根结点必须为黑
return;
}
//根结点非空,则遍历
Node p = tree;
while (p != null) {
if (data > p.data) {
if (p.right == null) {
p.right = new Node(data);
return;
}
p = p.right;//如果右结点非空,继续遍历
} else { // data < p.data
if (p.left == null) {
p.left = new Node(data);
return;
}
p = p.left;
}
}
}
旋转
平衡因子
左子树和右子树高度之差被称作平衡因子
最小平衡子树
从新插入节点向上查找,以第一个 a b s ( 平 衡 因 子 ) > 1 节点为根的子树形成的二叉树,被称为最小不平衡子树
实质就是上下结点压缩结合,降低树的高度以到达平衡
LL(右旋)
往左子树的左结点插入
右旋:将父节点绕左儿子顺时针下压(右旋左顺)
LL的意思是向左子树(L)的左孩子(L)中插入新节点后导致不平衡,此时左子树的高度比右子树高2,这种情况下需要右旋操作,而不是说LL的意思是右旋,后面的也是一样。
我们将这种情况抽象出来,得到下图:
我们需要对节点y进行平衡的维护。步骤如下图所示:
x右边结点顺时针旋转
代码实现:
AVLNode rightRotate(AVLNode y){
AVLNode x = y.lchild; //即将返回的节点是y的左子节点,将替换原结点的结点返回
AVLNode t3 = x.rchild; //暂存需要被代替的子结点
x.rchild = y; //把y放进x的右子节点
y.lchild = t3; //把前面预存的放到y的左子节点
x.color=y.color;
//更新height
y.height = Math.max(getHeight(y.lchild),getHeight(y.rchild))+1;
x.height = Math.max(getHeight(x.lchild),getHeight(x.rchild))+1;
return x;
}
RR(左旋)
往右子树的右结点插入
左旋:将父节点绕右儿子逆时针下压(左旋右逆)
x左边的结点逆时针旋转
代码实现:
AVLNode leftRotate(AVLNode y){
AVLNode x = y.rchild;//返回替换原结点的结点
NAVLNode ode t2 = x.lchild;//暂存被替换的子节点
x.lchild= y;//替换子节点
y.rchild= t2;//获取被替换的子节点
x.color=y.color;
//更新height
y.height = Math.max(getHeight(y.lchild),getHeight(y.rchild))+1;
x.height = Math.max(getHeight(x.lchild),getHeight(x.rchild))+1;
return x;
}
LR
往左子树的右结点插入
RL
四种情况总结:
LL:新插入节点为最小不平衡子树根节点的左儿子的左子树上 → \rightarrow→ 右旋使其恢复平衡
RR:新插入节点为最小不平衡子树根节点的右儿子的右子树上 → \rightarrow→ 左旋使其恢复平衡
LR:新插入节点为最小不平衡子树根节点的左儿子的右子树上 → \rightarrow→ 以左儿子为根节点进行左旋,再按原始的根节点右旋
RL:新插入节点为最小不平衡子树根节点的右儿子的左子树上 → \rightarrow→ 以右儿子为根节点进行右旋,再按原始的根节点左旋
删除
顾名思义,红黑树中的节点,一类被标记为黑色,一类被标记为红色。
2-3-4树
1、2-3-4树所有的叶子结点都在同一层
2、结点只能是2结点或3结点或4结点
- 2结点:要么有2个子结点,要么没有子结点
- 3结点:要么有3个子结点,要么没有子结点
- 4结点:要么有4个子结点,要么没有子结点
B树
B+树
红黑树,B树,B+树在面试中为什么重要
因为这些是计算机,数据库存储数据的数据结构基础
算法:
什么是算法:
完成事件的方法
时间复杂度:
时间复杂度表示一个程序运行所需要的时间
我们一般并不需要得到详细的值,只是需要比较快慢的区别即可
2. 度量时间复杂度的两种方法
1)事后统计法
2)事先估计法
时间复杂度的大O标记法中可以省略系数,常数,低阶
时间复杂度种类:
用”频度“描述语句执行的次数
1,常量阶O(1):语句频度为常量;程序算法执行的时间是一个与n无关的常数(不一定是1),那么这个程序算法的时间复杂度就是常量阶O(1)
eg:for (i=0;i<l000; i++) {x++; s=0;}
1000也是常量
2,线性阶O(n):语句频度为n
eg;for (i=O; i<n; i++) {x++; s=O;)
3,平方阶O(n^2):语句频度最大为n^2
eg:
空间复杂度:
一个程序的空间复杂度是指运行完一个程序所需内存的大小,其包括两个部分。
a)固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。
b)可变空间,这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。
声明----多部分是原网站的知识借鉴