1. 数据结构与算法
数据结构是计算机存储、组织数据的方式,是相互之间存在一种或多种特定关系的数据元素集合。 通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关。
算法是特定问题求解步骤的描述,在计算机中表现为指令的有限序列,算法是独立存在的一种解决问题的方法和思想。对于算法而言,语言并不重要,重要的是思想。
算法和数据结构区别
数据结构只是静态的描述了数据元素之间的关系,高效的程序需要在数据结构的基础上设计和选择算法。
- 算法是为了解决实际问题而设计的。
- 数据结构是算法需要处理的问题载体。
- 数据结构与算法相辅相成。
算法的特性
算法具有五个基本的特性:输入、输出、有穷性、确定性和可行性
输入输出:算法具有零个或多个输入、至少有一个或多个输出。
有穷性:指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。
确定性:算法的每一步骤都有确定的含义,不会出现二义性。
可行性:算法的每一步都必须是可行的,也就是说,每一步都能通过执行有限次数完成。
1.1 数据结构分类
按照视点的不同,我们把数据结构分为逻辑结构和物理结构。
1.1.1逻辑结构
集合结构:
集合结构中的数据元素除了同属于一个集合外,他们之间没有其他关系。各个数据元素是平等的。他们共同属于同一个集合,数据结构中的集合关系类似于数学中的集合
线性结构:
线性结构中的数据元素之间是一对一的关系
树形结构:
树形结构中是数据元素之间存在一种一对多的层次关系
图形结构:
图形结构的数据元素是多对多的关系
1.1.2 物理结构(存储结构)
物理结构:是指数据的逻辑结构在计算机中的存储形式,共分为两种:顺序存储和链式存储。
顺序存储:
把数据元素存放在地址连续的存储单元里,其数据的逻辑关系和物理关系是一致的。
链式存储结构:
把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。数据元素的存储关系并不能反映其逻辑关系,因此需要用一个指针存放数据元素的地址,这样通过地址就可以找到相关数据的位置
2.线性表
线性结构是一种最简单且常用的数据结构。线性结构的基本特点是节点之间满足线性关系。本章讨论的动态数组、链表、栈、队列都属于线性结构。他们的共同之处,是节点中有且只有一个开始节点和终端节点。按这种关系,可以把它们的所有节点排列成一个线性序列。但是,他们分别属于几种不同的抽象数据类型实现,它们之间的区别,主要就是操作的不同。
线性表是零个或者多个数据元素的有限序列,数据元素之间是有顺序的,数据元素个数是有限的,数据元素的类型必须相同。
线性表的性质:
1)a0 为线性表的第一个元素,只有一个后继。
2)an 为线性表的最后一个元素,只有一个前驱。
3)除 a0 和 an 外的其它元素 ai,既有前驱,又有后继。
4)线性表能够逐项访问和顺序存取。
线性表的抽象数据类型定义:
ADT线性表(List)
Data
线性表的数据对象集合为{ a1, a2, ……, an },每个元素的类型均为DataType。
其中,除第一个元素a1外,每个元素有且只有一个直接前驱元素,
除了最后一个元素an外,每个元素有且只有一个直接后继元素。
数据元素之间的关系是一一对应的。
Operation(操作)
// 初始化,建立一个空的线性表L。
InitList(*L);
// 若线性表为空,返回true,否则返回false
ListEmpty(L);
// 将线性表清空
ClearList(*L);
// 将线性表L中的第i个位置的元素返回给e
GetElem(L, i, *e);
// 在线性表L中的第i个位置插入新元素e
ListInsert(*L, i, e);
// 删除线性表L中的第i个位置元素,并用e返回其值
ListDelete(*L, i, *e);
// 返回线性表L的元素个数
ListLength(L);
// 销毁线性表
DestroyList(*L);
2.1 线性表顺序存储(动态数组)
通常线性表可以采用顺序存储和链式存储。采用顺序存储是表示线性表最简单的方法,具体做法是:将线性表中的元素一个接一个的存储在一块连续的存储区域中,这种顺序表示的线性表也成为顺序表。
设计与实现操作要点:
插入元素算法
- 判断线性表是否合法
- 判断插入位置是否合法
- 判断空间是否满足
- 把最后一个元素到插入位置的元素后移一个位置
- 将新元素插入
- 线性表长度加1
获取元素操作 - 判断线性表是否合法
- 判断位置是否合法
- 直接通过数组下标的方式获取元素
删除元素算法
- 判断线性表是否合法
- 判断删除位置是否合法
- 将元素取出
- 将删除位置后的元素分别向前移动一个位置
- 线性表长度减1
元素的插入
元素的删除
注意: 链表的容量和链表的长度是两个不同的概念
动态数组优点和缺点
优点:
无需为线性表中的逻辑关系增加额外的空间。
可以快速的获取表中合法位置的元素。
缺点:
插入和删除操作需要移动大量元素。
动态数组
# define _CRT_SECURE_NO_VARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
//动态数组结构体
struct dynamicArray
{
void** pAddr; //维护真实在堆区创建的数组的指针
int m_capacity; //数组容量
int m_size; //数组大小
};
//初始化数组
struct dynamicArray * int_DynamicArray(int capacity)
{
if(capacity<=0)
{
return NULL;
}
//给数组分配空间
struct dynamicArray * array=malloc(sizeof(struct dynamicArray));
if(array==NULL)
{
return NULL:
}
//给数组初始化
array->pAddr=malloc(sizeof(void*)* capacity);
array->m_capacity=capacity;
array->m_size=0;
return array;
}
//插入数组
void insert_DynamicArray(struct dynamicArray * array, int pos, void *data)
{
if(array==NULL)
{
return;
}
if(data==NULL)
{
return;
}
//无效位置 尾插
if(pos<0 || pos > array->m_size)
{
pos=array->m_size;
}
//判断是否满了,如果满- 动态扩展
if(array-> m_size==array->m_capacity)
{
//1.计算新空间大小
int newCapacity=array->m_capacity*2;
//2.创建新空间
void ** newSpace=malloc(sizeof(void*)*newCapacity);
//3.将原有数据拷贝到新空间下
memcpy(newSpace, array->pAddr, sizeof(void *)* array->m_capacity);
//4.释放原有内存空间
free(array->pAddr);
//5.更新新空间指向
array->pAddr=newSpace;
//6.更新新容量
array->m_capacity=newCapacity;
}
//插入新元素
//移动元素 进行插入新元素
for(int i=array->m_size-1;i>=pos;i--)
{
// 数据向后移动
array->pAddr[pos]=data;
//更新大小
array->m_size++;
}
//遍历数组
void foreach_DynamicArray(struct dynamicArray * array, void(*myPrint)(void*))
{
if(array==NULL)
{
return;
}
if(myPrint==NULL)
{
return;
}
for(int i=0;i<array-> m_size;i++)
{
myPrint(array->pAddr[i]);
}
}
//删除数组 按位置删除
void removeByPos_DynamicArray(struct dynamicArray * array , int pos)
{
if(NULL==array)
{
return;
}
if(pos <0 ||pos>array->m_size-1)
{
return;
}
//数据前移
for (int i=pos; i<array->m_size-1;i++)
{
array->pAddr[i]=array->pAddr[i+1];
}
//更新数组大小
array->m_size--;
}
//按值删除数据
void removeByValue_DynamicArray(struct dynamicArray * array , void * data , int (* myCompare)(void * ,void *))
{
if ( array == NULL)
{
return;
}
if ( data == NULL)
{
return;
}
for (int i = 0; i < array->m_size;i++)
{
if (myCompare(array->pAddr[i], data))
{
//如果找到要删除的数据,i就是要删除的具体位置
removeByPos_DynamicArray(array, i);
break;
}
}
}
//销毁数组
void destroy_DynamicArray(struct dynamicArray *array)
{
if(array==NULL)
{
return;
)
if(array->pAddr !=NULL)
{
free(array->pAddr);
array->pAddr=NULL;
}
free(array);
array=NULL;
}
//测试
struct Person
{
char name[64];
int age;
};
void myPrintPerson(void *data)
{
struct Person *p=data;
printf("姓名: %s 年龄: %d\n", p->name,p->age);
}
int myComparePerson(void * data1, void *data2)
{
struct Person * p1 = data1;
struct Person * p2 = data2;
return strcmp(p1->name, p2->name) == 0 && p1->age == p2->age;
}
int main(){
//初始化动态数组
struct dynamicArray * array = init_DynamicArray(5);
//准备数据
struct Person p1 = { "亚瑟", 18 };
struct Person p2 = { "妲己", 20 };
struct Person p3 = { "安琪拉", 19 };
struct Person p4 = { "凯", 21 };
struct Person p5 = { "孙悟空", 999 };
struct Person p6 = { "李白", 999};
printf("插入数据前: 容量:%d 大小:%d\n", array->m_capacity, array->m_size);
//插入数据
insert_DynamicArray(array, 0, &p1);
insert_DynamicArray(array, 0, &p2);
insert_DynamicArray(array, 1, &p3);
insert_DynamicArray(array, 0, &p4);
insert_DynamicArray(array, -1, &p5);
insert_DynamicArray(array, 2, &p6);
// 凯 妲己 李白 安琪拉 亚瑟 孙悟空
//遍历数据
foreach_DynamicArray(array, myPrintPerson);
printf("插入数据后: 容量:%d 大小:%d\n", array->m_capacity, array->m_size);
//测试删除 按位置删除
removeByPos_DynamicArray(array, 2);
printf("---------------------\n");
foreach_DynamicArray(array, myPrintPerson);
printf("删除数据后: 容量:%d 大小:%d\n", array->m_capacity, array->m_size);
struct Person p = { "亚瑟", 18 };
removeByValue_DynamicArray(array, &p, myComparePerson);
printf("---------------------\n");
foreach_DynamicArray(array, myPrintPerson);
printf("删除数据后: 容量:%d 大小:%d\n", array->m_capacity, array->m_size);
//销毁数组
destroy_DynamicArray(array);
array = NULL;
system("pause");
return EXIT_SUCCESS;
}
2.2 线性表的链式存储(单向链表)
链表为了表示每个数据元素与其直接后继元素之间的逻辑关系,每个元素除了存储本身的信息外,还需要存储指示其直接后继的信息。
单链表
- 线性表的链式存储结构中,每个节点中只包含一个指针域,这样的链表叫单链表。
- 通过每个节点的指针域将线性表的数据元素按其逻辑次序链接在一起(如图)。
表头结点
链表中的第一个结点,包含指向第一个数据元素的指针以及链表自身的一些信息
数据结点
链表中代表数据元素的结点,包含指向下一个数据元素的指针和数据元素的信息
尾结点
链表中的最后一个数据结点,其下一元素指针为空,表示无后继。
- 插入操作
node->next = current->next;
current->next = node;
- 删除操作
current->next = ret->next;
优点:
无需一次性定制链表的容量
插入和删除操作无需移动数据元素
缺点:
数据元素必须保存后继元素的位置信息
获取指定数据的元素操作需要顺序访问之前的元素
单向链表-企业版
设计:节点只维护指针域,用户数据预留前4个字节由底层使用
//节点结构体
struct LinkNode
{
//只维护指针域
struct LinkNode * next;
};
//链表结构体
struct LList
{
//头节点
struct LinkNode pHeader;
//链表长度
int m_Size;
};
typedef void * LinkList;
//初始化链表
LinkList init_LinkList()
{
struct LList * myList = malloc(sizeof(struct LList));
if (myList == NULL)
{
return NULL;
}
myList->pHeader.next = NULL;
myList->m_Size = 0;
return myList;
}
//插入链表
void insert_LinkList(LinkList list , int pos , void * data)
{
if (list == NULL)
{
return;
}
if (data == NULL)
{
return;
}
//将list还原 struct LList数据类型
struct LList * myList = list;
if (pos < 0 || pos >myList->m_Size - 1)
{
//无效位置进行尾插
pos = myList->m_Size;
}
//用户数据前4个字节 由我们来使用
struct LinkNode * myNode = data;
//找插入节点的前驱节点
struct LinkNode * pCurrent = &myList->pHeader;
for (int i = 0; i < pos;i++)
{
pCurrent = pCurrent->next;
}
//pCurrent是前驱节点位置
//更改指针指向
myNode->next = pCurrent->next;
pCurrent->next = myNode;
//更新链表长度
myList->m_Size++;
}
//遍历链表
void foreach_LinkList(LinkList list, void(*myForeach)(void *))
{
if (list == NULL)
{
return;
}
struct LList * myList = list;
struct LinkNode * myNode = myList->pHeader.next;
for (int i = 0; i < myList->m_Size;i++)
{
myForeach(myNode);
myNode = myNode->next;
}
}
//删除节点 按位置删除
void removeByPos_ListList( LinkList list, int pos)
{
if (list == NULL)
{
return;
}
struct LList * mylist = list;
if (pos < 0 || pos > mylist->m_Size - 1)
{
return;
}
//找待删除节点的前驱位置
struct LinkNode * pCurrent = &mylist->pHeader;
for (int i = 0; i < pos;i++)
{
pCurrent = pCurrent->next;
}
//记录待删除节点
struct LinkNode * pDel = pCurrent->next;
//更改指针指向
pCurrent->next = pDel->next;
//free(pDel); //数据是用户管理开辟的,用户管理释放
//更新长度
mylist->m_Size--;
}
//销毁数组
void destroy_LinkList( LinkList list)
{
if (list == NULL)
{
return;
}
free(list);
list = NULL;
}
//测试
struct Person
{
void * node;
char name[64];
int age;
};
void myPrintPerson(void * data)
{
struct Person * p = data;
printf("姓名: %s 年龄: %d \n", p->name, p->age);
}
void test01()
{
//初始化链表
LinkList mylist = init_LinkList();
//创建数据
struct Person p1 = { NULL,"aaa", 10 };
struct Person p2 = { NULL,"bbb", 20 };
struct Person p3 = { NULL,"ccc", 30 };
struct Person p4 = { NULL,"ddd", 40 };
struct Person p5 = { NULL,"eee", 50 };
//插入节点
insert_LinkList(mylist, 0, &p1);
insert_LinkList(mylist, 0, &p2);
insert_LinkList(mylist, 1, &p3);
insert_LinkList(mylist, -1, &p4);
insert_LinkList(mylist, 0, &p5);
//遍历链表
// eee bbb ccc aaa ddd
foreach_LinkList(mylist, myPrintPerson);
//删除 aaa
removeByPos_ListList(mylist, 3);
printf("-----------------------\n");
foreach_LinkList(mylist, myPrintPerson);
//销毁数组
destroy_LinkList(mylist);
mylist = NULL;
}
int main(){
test01();
system("pause");
return EXIT_SUCCESS;
}
3.受限线性表
3.1 栈
首先它是一个线性表,也就是说,栈元素具有线性关系,即前驱后继关系。只不过它是一种特殊的线性表而已。定义中说是在线性表的表尾进行插入和删除操作,这里表尾是指栈顶,而不是栈底。
它的特殊之处在于限制了这个线性表的插入和删除的位置,它始终只在栈顶进行。这也就使得:栈底是固定的,最先进栈的只能在栈底。
- 栈不可以遍历,只有栈顶元素可以被外界访问到,栈是先进后出的数据结构。
- 遍历:不重复不遗漏访问容器中的所有数据,遍历算法属于非质变算法。
操作
- 栈的插入操作,叫做进栈,也成压栈。类似子弹入弹夹(如下图所示)
- 栈的删除操作,叫做出栈,也有的叫做弾栈,退栈。如同弹夹中的子弹出夹(如下图所示)
3.1.2 栈的顺序存储
栈的顺序存储结构简称顺序栈,它是运算受限制的顺序表。顺序栈的存储结构是:利用一组地址连续的的存储单元依次存放自栈底到栈顶的数据元素,同时附设指针top只是栈顶元素在顺序表中的位置。
栈顶设计在数组的尾地址,因为数组尾部进行频繁插入删除效率会比头部高。
因为栈是一种特殊的线性表,所以栈的顺序存储可以通过顺序线性表来实现。
- 利用数组模拟出 先进后出数据结构
- 数组中首地址 做栈底 方便数组尾部做插入删除
- 对外接口
初始化栈 init
入栈 push
出栈 pop
栈顶 top
栈大小 size
是否为空 isEmpty
销毁栈 destroy
#define MAX 1024
struct SStack
{
void * data[MAX]; //栈的数组
int m_Size; //栈大小
};
typedef void * SeqStack;
//初始化栈
SeqStack init_SeqStack()
{
struct SStack * myStack = malloc(sizeof(struct SStack));
if (myStack == NULL)
{
return NULL;
}
//初始化数组
memset(myStack->data, 0, sizeof(void *)* MAX);
//初始化栈大小
myStack->m_Size = 0;
return myStack;
}
//入栈
void push_SeqStack(SeqStack stack , void * data)
{
//入栈本质 --- 数组尾插
if (stack == NULL)
{
return;
}
if ( data == NULL)
{
return;
}
struct SStack * mystack = stack;
if (mystack->m_Size == MAX)
{
return;
}
mystack->data[mystack->m_Size] = data;
mystack->m_Size++;
}
//出栈
void pop_SeqStack(SeqStack stack)
{
//出栈本质 --- 数组尾删
if (stack == NULL)
{
return;
}
struct SStack * mystack = stack;
if (mystack->m_Size == 0)
{
return;
}
mystack->data[mystack->m_Size - 1] = NULL;
mystack->m_Size--;
}
//返回栈顶
void * top_SeqStack(SeqStack stack)
{
if (stack == NULL)
{
return NULL;
}
struct SStack * mystack = stack;
if (mystack->m_Size == 0)
{
return NULL;
}
return mystack->data[mystack->m_Size - 1];
}
//返回栈大小
int size_SeqStack(SeqStack stack)
{
if (stack == NULL)
{
return -1;
}
struct SStack * mystack = stack;
return mystack->m_Size;
}
//判断栈是否为空
int isEmpty_SeqStack(SeqStack stack)
{
if (stack == NULL)
{
return -1;//返回-1代表真 空栈
}
struct SStack * mystack = stack;
if (mystack->m_Size == 0)
{
return 1;
}
return 0; //返回0 代表 不是空栈
}
//销毁栈
void destroy_SeqStack(SeqStack stack)
{
if (stack == NULL)
{
return;
}
free(stack);
stack = NULL;
}
//测试
struct Person
{
char name[64];
int age;
};
void test01()
{
//初始化栈
SeqStack myStack = init_SeqStack();
//创建数据
struct Person p1 = { "aaa", 10 };
struct Person p2 = { "bbb", 20 };
struct Person p3 = { "ccc", 30 };
struct Person p4 = { "ddd", 40 };
struct Person p5 = { "eee", 50 };
//入栈
push_SeqStack(myStack, &p1);
push_SeqStack(myStack, &p2);
push_SeqStack(myStack, &p3);
push_SeqStack(myStack, &p4);
push_SeqStack(myStack, &p5);
printf("栈的元素个数为:%d\n", size_SeqStack(myStack));
while (isEmpty_SeqStack(myStack) == 0) //栈不为空,查看栈顶元素,出栈
{
struct Person * p = top_SeqStack(myStack);
printf("姓名:%s 年龄:%d\n", p->name, p->age);
//出栈
pop_SeqStack(myStack);
}
printf("栈的元素个数为:%d\n", size_SeqStack(myStack));
//销毁栈
destroy_SeqStack(myStack);
}
int main(){
test01();
system("pause");
return EXIT_SUCCESS;
}
3.1.3 栈的链式存储
栈的链式存储结构简称链栈,栈只是栈顶来做插入和删除操作,栈顶放在链表的头部。由于单链表有头指针,而栈顶指针也是必须的,那干嘛不让他俩合二为一呢,所以比较好的办法就是把栈顶放在单链表的头部。另外都已经有了栈顶在头部了,单链表中比较常用的头结点也就失去了意义,通常对于链栈来说,是不需要头结点的。
- 利用链表模拟出 先进后出的数据结构
- 头节点端做栈顶 比较方便做入栈和出栈
- 对外接口(跟顺序存储一样)
//节点结构体
struct stackNode
{
struct stackNode * next;
};
//栈的结构体
struct LStack
{
struct stackNode pHeader;
int m_size;
};
typedef void * LinkStack;
//初始化
LinkStack init_LinkStack()
{
struct LStack * myStack = malloc(sizeof( struct LStack));
if (myStack == NULL)
{
return NULL;
}
myStack->pHeader.next = NULL;
myStack->m_size = 0;
return myStack;
}
//入栈
void push_LinkStack( LinkStack stack , void * data)
{
//入栈本质 --- 链表头插
if (stack == NULL)
{
return;
}
if (data == NULL)
{
return;
}
struct LStack * myStack = stack;
//将用户数据 取出前4字节用
struct stackNode * myNode = data;
//更改指针指向
myNode->next = myStack->pHeader.next;
myStack->pHeader.next = myNode;
//更新链表长度
myStack->m_size++;
}
//出栈
void pop_LinkStack(LinkStack stack)
{
//出栈本质 --- 链表头删
if (stack == NULL)
{
return;
}
struct LStack * myStack = stack;
if (myStack->m_size == 0)
{
return;
}
//更改指针指向
//缓存第一个有数据节点
struct stackNode * pFirst = myStack->pHeader.next;
myStack->pHeader.next = pFirst->next;
//更新栈大小
myStack->m_size--;
}
//返回栈顶元素
void * top_LinkStack(LinkStack stack)
{
if (stack == NULL)
{
return NULL;
}
struct LStack * myStack = stack;
if (myStack->m_size == 0)
{
return NULL;
}
return myStack->pHeader.next;
}
//返回栈个数
int size_LinkStack(LinkStack stack)
{
if (stack == NULL)
{
return -1;
}
struct LStack * myStack = stack;
return myStack->m_size;
}
//判断是否为空
int isEmpty_LinkStack(LinkStack stack)
{
if (stack == NULL)
{
return -1;
}
struct LStack * myStack = stack;
if (myStack->m_size == 0)
{
return 1;
}
return 0;
}
//销毁
void destroy_LinkStack(LinkStack stack)
{
if (stack == NULL)
{
return;
}
free(stack);
stack = NULL;
}
//测试
struct Person
{
void * node;
char name[64];
int age;
};
void test01()
{
//初始化栈
LinkStack myStack = init_LinkStack();
//创建数据
struct Person p1 = { NULL, "aaa", 10 };
struct Person p2 = { NULL, "bbb", 20 };
struct Person p3 = { NULL, "ccc", 30 };
struct Person p4 = { NULL, "ddd", 40 };
struct Person p5 = { NULL, "eee", 50 };
//入栈
push_LinkStack(myStack, &p1);
push_LinkStack(myStack, &p2);
push_LinkStack(myStack, &p3);
push_LinkStack(myStack, &p4);
push_LinkStack(myStack, &p5);
printf("链式存储-- 栈的元素个数为:%d\n", size_LinkStack(myStack));
while (isEmpty_LinkStack(myStack) == 0) //栈不为空,查看栈顶元素,出栈
{
struct Person * p = top_LinkStack(myStack);
printf("姓名:%s 年龄:%d\n", p->name, p->age);
//出栈
pop_LinkStack(myStack);
}
printf("链式存储-- 栈的元素个数为:%d\n", size_LinkStack(myStack));
//销毁栈
destroy_LinkStack(myStack);
}
int main(){
test01();
system("pause");
return EXIT_SUCCESS;
}
3.1.4 栈的应用
就近匹配
几乎所有的编译器都具有检测括号是否匹配的能力,5+5*(6)+9/3*1)-(1+3(
- 算法思路
从第一个字符开始扫描
当遇见普通字符时忽略,
当遇见左括号时压入栈中
当遇见右括号时从栈中弹出栈顶符号,并进行匹配,然后舍弃括号
匹配成功:继续读入下一个字符
匹配失败:立即停止,并报错
结束:
成功: 所有字符扫描完毕,且栈为空
失败:匹配失败或所有字符扫描完毕但栈非空
int isLeft(char ch)
{
return ch == '(';
}
int isRight(char ch)
{
return ch == ')';
}
void printError(char * str, char * errMsg , char * pos)
{
printf("错误信息:%s\n", errMsg);
printf("%s\n", str);
//计算打印空格数量
int num = pos - str;
for (int i = 0; i < num;i++)
{
printf(" ");
}
printf("|\n");
}
void test01()
{
char * str = "5+5*(6)+9/3*1)-(1+3(";
//char * str = "5+5*(6)+9/3*1-(1+3(";
char * p = str;
//初始化栈
SeqStack myStack = init_SeqStack();
while ( *p != '\0')
{
//如果是左括号,入栈
if (isLeft(*p))
{
//入栈
push_SeqStack(myStack, p);
}
//如果是右括号
if (isRight(*p))
{
//栈中有元素 出栈
if (size_SeqStack(myStack) > 0)
{
pop_SeqStack(myStack);
}
else
{
//右括号没有匹配到对应的左括号,立即停止,并报错
printError(str,"右括号没有匹配到对应的左括号!", p);
break;
}
}
p++;
}
//遍历结束 判断是否有 左括号没有匹配到对应的右括号
while (size_SeqStack(myStack) > 0)
{
printError(str, "左括号没有匹配到对应的右括号!", top_SeqStack(myStack));
//出栈
pop_SeqStack(myStack);
}
//销毁栈
destroy_SeqStack(myStack);
myStack = NULL;
}
int main(){
test01();
system("pause");
return EXIT_SUCCESS;
}
中缀表达式和后缀表达式
运算符放在数字后面 ===》 符合计算机运算:
5 + 4 => 5 4 +
1 + 2 * 3 => 1 2 3 * +
8 +( 3 – 1 ) * 5 => 8 3 1 – 5 * +
中缀转后缀算法
遍历中缀表达式中的数字和符号
- 对于数字:直接输出
- 对于符号:
-左括号:进栈
-运算符号:与栈顶符号进行优先级比较
若栈顶符号优先级低:此符号进栈 (默认栈顶若是左括号,左括号优先级最低)
若栈顶符号优先级不低:将栈顶符号弹出并输出,之后进栈 - 右括号:将栈顶符号弹出并输出,直到匹配左括号,将左括号和右括号同时舍弃
遍历结束:将栈中的所有符号弹出并输出
基于后缀表达式计算
例如:8 3 1 – 5 * +
计算规则
遍历后缀表达式中的数字和符号
对于数字:进栈
对于符号:
从栈中弹出右操作数
从栈中弹出左操作数
根据符号进行运算
将运算结果压入栈中
遍历结束:栈中的唯一数字为计算结果
3.2 队列
队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。队列是一种先进先出的t(First In First Out)的线性表,简称FIFO。允许插入的一端为队尾,允许删除的一端为队头。队列不允许在中间部位进行操作!
队列的顺序存储
可以用线性表顺序存储来模拟队列
- 利用数组模拟出 先进先出的数据结构
- 接口:
初始化队列 init
入队 push
出队 pop
返回队列大小 size
判断是否为空 isEmpty
队头元素 front
队尾元素 back
销毁队列 destroy
//队列C文件
//初始化队列
seqQueue init_SeqQueue()
{
struct dynamicArray * arr = init_DynamicArray(MAX);
return arr;
}
//入队
void push_SeqQueue(seqQueue queue, void * data)
{
//本质 尾插
if (queue == NULL)
{
return;
}
if ( data == NULL)
{
return;
}
struct dynamicArray * myQueue = queue;
if (myQueue->m_size == MAX)
{
return;
}
insert_DynamicArray(myQueue, myQueue->m_size, data);
}
//出队
void pop_SeqQueue(seqQueue queue)
{
//本质 头删
if (queue == NULL)
{
return;
}
struct dynamicArray * myQueue = queue;
if (myQueue->m_size <= 0 )
{
return;
}
removeByPos_DynamicArray(myQueue, 0);
}
//返回队列大小
int size_SeqQueue(seqQueue queue)
{
if (queue == NULL)
{
return -1;
}
struct dynamicArray * myQueue = queue;
return myQueue->m_size;
}
//判断队列是否为空
int isEmpty_SeqQueue(seqQueue queue)
{
if (queue == NULL)
{
return -1;
}
struct dynamicArray * myQueue = queue;
if (myQueue->m_size == 0)
{
return 1;
}
return 0;
}
//返回队头元素
void * front_SeqQueue(seqQueue queue)
{
if (queue == NULL)
{
return NULL;
}
struct dynamicArray * myQueue = queue;
return myQueue->pAddr[0];
}
//返回队尾元素
void * back_SeqQueue(seqQueue queue)
{
if (queue == NULL)
{
return NULL;
}
struct dynamicArray * myQueue = queue;
return myQueue->pAddr[myQueue->m_size-1];
}
//销毁队列
void destroy_SeqQueue(seqQueue queue)
{
if (queue == NULL)
{
return;
}
destroy_DynamicArray(queue);
}
//顺序存储
struct Person
{
char name[64];
int age;
};
void test01()
{
//初始化队列
seqQueue myQueue= init_SeqQueue();
//准备数据
struct Person p1 = { "aaa", 10 };
struct Person p2 = { "bbb", 20 };
struct Person p3 = { "ccc", 30 };
struct Person p4 = { "ddd", 40 };
//入队
push_SeqQueue(myQueue, &p1);
push_SeqQueue(myQueue, &p2);
push_SeqQueue(myQueue, &p3);
push_SeqQueue(myQueue, &p4);
printf("队列大小为:%d\n", size_SeqQueue(myQueue));
while ( isEmpty_SeqQueue(myQueue) == 0)
{
//访问队头
struct Person * pFront = front_SeqQueue(myQueue);
printf("队头元素 -- 姓名:%s 年龄: %d\n", pFront->name, pFront->age);
//访问队尾
struct Person * pBack = back_SeqQueue(myQueue);
printf("队尾元素 -- 姓名:%s 年龄: %d\n", pBack->name, pBack->age);
//出队
pop_SeqQueue(myQueue);
}
printf("队列大小为:%d\n", size_SeqQueue(myQueue));
//销毁队列
destroy_SeqQueue(myQueue);
}
int main(){
test01();
system("pause");
return EXIT_SUCCESS;
}
队列的链式存储
可以用线性表链式存储来模拟队列的链式存储
- 利用链表模拟出 先进先出数据结构
- 设计:
节点 只维护指针域
队列结构体:
struct QueueNode pHeader; 头节点
int m_Size; 队列大小
struct QueueNode * pTail; 尾节点指针 - 接口:
初始化队列 init
入队 push
出队 pop
返回队列大小 size
判断是否为空 isEmpty
队头元素 front
队尾元素 back
销毁队列 destroy
链式存储代码查看上一节
struct Person
{
void * node;
char name[64];
int age;
};
void test01()
{
//初始化队列
LinkQueue myQueue = init_LinkQueue();
//准备数据
struct Person p1 = { NULL,"aaa", 10 };
struct Person p2 = { NULL,"bbb", 20 };
struct Person p3 = { NULL,"ccc", 30 };
struct Person p4 = { NULL,"ddd", 40 };
//入队
push_LinkQueue(myQueue, &p1);
push_LinkQueue(myQueue, &p2);
push_LinkQueue(myQueue, &p3);
push_LinkQueue(myQueue, &p4);
printf("队列大小为:%d\n", size_LinkQueue(myQueue));
while (isEmpty_LinkQueue(myQueue) == 0)
{
//访问队头
struct Person * pFront = front_LinkQueue(myQueue);
printf("链式存储::队头元素 -- 姓名:%s 年龄: %d\n", pFront->name, pFront->age);
//访问队尾
struct Person * pBack = back_LinkQueue(myQueue);
printf("链式存储::队尾元素 -- 姓名:%s 年龄: %d\n", pBack->name, pBack->age);
//出队
pop_LinkQueue(myQueue);
}
printf("队列大小为:%d\n", size_LinkQueue(myQueue));
//销毁队列
destroy_LinkQueue(myQueue);
}
int main(){
test01();
system("pause");
return EXIT_SUCCESS;
}
4. 树和二叉树
4.1 树的定义
由一个或多个(n≥0)结点组成的有限集合T,有且仅有一个结点称为根(root),当n>1时,其余的结点分为m(m≥0)个互不相交的有限集合T1,T2,…,Tm。每个集合本身又是棵树,被称作这个根的子树 。
树的结构特点
- 非线性结构,有一个直接前驱,但可能有多个直接后继(1:n)
- 树的定义具有递归性,树中还有树。
- 树可以为空,即节点个数为0
根 即根结点(没有前驱)
叶子 即终端结点(没有后继)
森林 指m棵不相交的树的集合(例如删除A后的子树个数)
有序树 结点各子树从左至右有序,不能互换(左为第一)
无序树 结点各子树可互换位置。
双亲 即上层的那个结点(直接前驱) parent
孩子 即下层结点的子树 (直接后继) child
兄弟 同一双亲下的同层结点(孩子之间互称兄弟)sibling
堂兄弟 即双亲位于同一层的结点(但并非同一双亲)cousin
祖先 即从根到该结点所经分支的所有结点
子孙 即该结点下层子树中的任一结点
结点 即树的数据元素
结点的度 结点挂接的子树数(有几个直接后继就是几度)
结点的层次 从根到该结点的层数(根结点算第一层)
终端结点 即度为0的结点,即叶子
分支结点 除树根以外的结点(也称为内部结点)
树的度 所有结点度中的最大值(Max{各结点的度})
树的深度(或高度) 指所有结点中最大的层数(Max{各结点的层次})
上图中的结点数= 13,树的度= 3,树的深度= 4
树的表示法
图形表示法、广义表表示法、左孩子右兄弟表示法
4.2 二叉树
定义:
n(n≥0)个结点的有限集合,由一个根结点以及两棵互不相交的、分别称为左子树和右子树的二叉树组成 。
逻辑结构:
一对二(1:2)
基本特征:
- 每个结点最多只有两棵子树(不存在度大于2的结点);
- 左子树和右子树次序不能颠倒(有序树)。
二叉树性质
- 性质1: 在二叉树的第i层上至多有2i-1个结点(i>0)
- 性质2: 深度为k的二叉树至多有2k-1个结点(k>0)
- 性质3: 对于任何一棵二叉树,若度为2的结点数有n2个,则叶子数(n0)必定为n2+1 (即n0=n2+1)
满二叉树
一棵深度为k 且有2k -1个结点的二叉树。
特点:每层都“充满”了结点
完全二叉树
除最后一层外,每一层上的节点数均达到最大值;在最后一层上只缺少右边的若干结点。理解:k-1层与满二叉树完全相同,第k层结点尽力靠左
二叉树的表示
- 二叉链表示法
一般从根结点开始存储。相应地,访问树中结点时也只能从根开始。
存储结构
结点数据类型定义:
typedef struct BiTNode
{
int data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
4.3 二叉树的遍历
遍历定义:指按某条搜索路线遍访每个结点且不重复(又称周游)。
- 遍历用途
它是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础和核心。 - 遍历方法
牢记一种约定,对每个结点的查看都是“先左后右” 。
限定先左后右,树的遍历有三种实现方案:
DLR LDR LRD
先 (根)序遍历 中 (根)序遍历 后(根)序遍历
DLR — 先序遍历,即先根再左再右
LDR — 中序遍历,即先左再根再右
LRD — 后序遍历,即先左再右再根
注:“先、中、后”的意思是指访问的结点D是先于子树出现还是后于子树出现。从递归的角度看,这三种算法是完全相同的,或者说这三种遍历算法的访问路径是相同的,只是访问结点的时机不同。
struct BinaryNode
{
//数据域
char ch;
//指针域
struct BinaryNode * lChild;
struct BinaryNode * rChild;
};
void recursion(struct BinaryNode * root)
{
if (root == NULL)
{
return;
}
//先序遍历
printf("%c ", root->ch);
recursion(root->lChild);
recursion(root->rChild);
}
void test01()
{
struct BinaryNode nodeA = { 'A', NULL, NULL };
struct BinaryNode nodeB = { 'B', NULL, NULL };
struct BinaryNode nodeC = { 'C', NULL, NULL };
struct BinaryNode nodeD = { 'D', NULL, NULL };
struct BinaryNode nodeE = { 'E', NULL, NULL };
struct BinaryNode nodeF = { 'F', NULL, NULL };
struct BinaryNode nodeG = { 'G', NULL, NULL };
struct BinaryNode nodeH = { 'H', NULL, NULL };
//建立关系
nodeA.lChild = &nodeB;
nodeA.rChild = &nodeF;
nodeB.rChild = &nodeC;
nodeC.lChild = &nodeD;
nodeC.rChild = &nodeE;
nodeF.rChild = &nodeG;
nodeG.lChild = &nodeH;
//递归遍历
recursion(&nodeA);
}
int main(){
test01();
system("pause");
return EXIT_SUCCESS;
}
二叉树编程
- 求二叉树叶子数量
左子树与右子树都同时为NULL,称为叶子 - 求二叉树高度
左子树高度 与 右子树高度比 ,取大的值 +1 就是这个树的高度
- 拷贝二叉树
先拷贝左子树
再拷贝右子树
再创建根节点 挂载拷贝出的左右子树,返回给用户
- 释放二叉树
利用递归特性释放二叉树
struct BinaryNode
{
//数据域
char ch;
//指针域
struct BinaryNode * lChild;
struct BinaryNode * rChild;
};
void calculateLeafNum(struct BinaryNode * root, int * p)
{
if (root == NULL)
{
return;
}
//如果节点 左子树 与右子树 同时为空 称为叶子
if (root->lChild == NULL && root->rChild == NULL)
{
(*p)++;
}
calculateLeafNum(root->lChild, p);
calculateLeafNum(root->rChild, p);
}
//获取树高度
int getTreeHeight(struct BinaryNode * root)
{
if (root == NULL)
{
return 0;
}
//获取左子树高度
int lHeight = getTreeHeight(root->lChild);
//获取右子树高度
int rHeight = getTreeHeight(root->rChild);
//从左子树和右子树中取大的值+1
int height = lHeight > rHeight ? lHeight + 1 : rHeight + 1;
return height;
}
//拷贝二叉树
struct BinaryNode * copyTree(struct BinaryNode * root)
{
if (root ==NULL)
{
return NULL;
}
//先拷贝左子树
struct BinaryNode * lChild = copyTree(root->lChild);
//再拷贝右子树
struct BinaryNode * rChild = copyTree(root->rChild);
struct BinaryNode * newNode = malloc(sizeof(struct BinaryNode));
newNode->ch = root->ch;
newNode->lChild = lChild;
newNode->rChild = rChild;
return newNode;
}
void recursion(struct BinaryNode * root)
{
if (root == NULL)
{
return;
}
printf("%c ", root->ch);
recursion(root->lChild);
recursion(root->rChild);
}
void freeTree(struct BinaryNode * root)
{
if (root == NULL)
{
return;
}
//先释放左子树
freeTree(root->lChild);
//再释放右子树
freeTree(root->rChild);
//释放根
printf("%c被释放了\n", root->ch);
free(root);
}
void test01()
{
struct BinaryNode nodeA = { 'A', NULL, NULL };
struct BinaryNode nodeB = { 'B', NULL, NULL };
struct BinaryNode nodeC = { 'C', NULL, NULL };
struct BinaryNode nodeD = { 'D', NULL, NULL };
struct BinaryNode nodeE = { 'E', NULL, NULL };
struct BinaryNode nodeF = { 'F', NULL, NULL };
struct BinaryNode nodeG = { 'G', NULL, NULL };
struct BinaryNode nodeH = { 'H', NULL, NULL };
//建立关系
nodeA.lChild = &nodeB;
nodeA.rChild = &nodeF;
nodeB.rChild = &nodeC;
nodeC.lChild = &nodeD;
nodeC.rChild = &nodeE;
nodeF.rChild = &nodeG;
nodeG.lChild = &nodeH;
//1、求二叉树 叶子数量
int num = 0;
calculateLeafNum(&nodeA, &num);
printf("树的叶子数量为:%d\n", num);
//2、 求树的高度/深度
int height = getTreeHeight( &nodeA);
printf("树的高度为:%d\n", height);
//3、 拷贝二叉树
struct BinaryNode * newTree = copyTree(&nodeA);
//递归遍历
recursion(newTree);
printf("\n");
//4、 释放二叉树
freeTree(newTree);
}
int main(){
test01();
system("pause");
return EXIT_SUCCESS;
}
4.4 二叉树的非递归遍历
利用栈容器可以实现二叉树的非递归遍历。首先将每个节点都设置一个标志,默认标志为假(F),根据节点的的状态进行如下流程。
执行上述流程,可以得到先序遍历的结果,如果想得到其他二叉树遍历结果,修改2.4步骤即可。
栈的代码查看上述章节
struct BinaryNode
{
//数据域
char ch;
//指针域
struct BinaryNode * lChild;
struct BinaryNode * rChild;
//标志
int flag;
};
void nonRecursion(struct BinaryNode * root)
{
//初始化栈
SeqStack myStack = init_SeqStack();
push_SeqStack(myStack, root);
while (size_SeqStack(myStack) > 0)
{
//获取栈顶元素
struct BinaryNode * pTop = top_SeqStack(myStack);
//出栈
pop_SeqStack(myStack);
//如果标志位真 直接输出 并且执行下一次循环
if (pTop->flag == 1)
{
printf("%c ", pTop->ch);
continue;
}
//如果为假 将标志改为真
pTop->flag = 1;
//将右子树 左子树 根 入栈
if (pTop->rChild != NULL)
{
push_SeqStack(myStack, pTop->rChild);
}
if (pTop->lChild != NULL)
{
push_SeqStack(myStack, pTop->lChild);
}
push_SeqStack(myStack, pTop);
}
//销毁栈
destroy_SeqStack(myStack);
}
void test01()
{
struct BinaryNode nodeA = { 'A', NULL, NULL,0 };
struct BinaryNode nodeB = { 'B', NULL, NULL,0 };
struct BinaryNode nodeC = { 'C', NULL, NULL,0 };
struct BinaryNode nodeD = { 'D', NULL, NULL,0 };
struct BinaryNode nodeE = { 'E', NULL, NULL,0 };
struct BinaryNode nodeF = { 'F', NULL, NULL,0 };
struct BinaryNode nodeG = { 'G', NULL, NULL,0 };
struct BinaryNode nodeH = { 'H', NULL, NULL,0 };
//建立关系
nodeA.lChild = &nodeB;
nodeA.rChild = &nodeF;
nodeB.rChild = &nodeC;
nodeC.lChild = &nodeD;
nodeC.rChild = &nodeE;
nodeF.rChild = &nodeG;
nodeG.lChild = &nodeH;
//非递归遍历
nonRecursion(&nodeA);
}
int main(){
test01();
system("pause");
return EXIT_SUCCESS;
}
5.查找
5.1 二叉排序树
二叉排序树(Binary Sort Tree)又称二叉查找(搜索)树(Binary Search Tree)。
它是一颗空树,或者是满足如下性质的二叉树:
①若它的左子树非空,则左子树上所有结点的值均小于根结点的值;
②若它的右子树非空,则右子树上所有结点的值均大于根结点的值;
③左、右子树本身又各是一棵二叉排序树。
- 特点(BST性质)
二叉排序树中任一结点x,其左(右)子树中任一结点y(若存在)的关键字必小(大)于x的关键字。
二叉排序树中,各结点关键字是惟一的。
按中序遍历该树所得到的中序序列是一个递增有序序列。
中序遍历二叉排序树可得到一个依据关键字的有序序列,一个无序序列可以通过构造一棵二叉排序树变成一个有序序列,构造树的过程即是对无序序列进行排序的过程。每次插入的新的结点都是二叉排序树上新的叶子结点,在进行插入操作时,不必移动其它结点,只需改动某个结点的指针,由空变为非空即可。
5.1.1 存储结构
存储结构
二叉排序树通常采用二叉链表作为存储结构。
结构定义
/* 二叉树的二叉链表结点结构定义 */
typedef struct BiTNode /* 结点结构 */
{
int data; /* 结点数据 */
struct BiTNode *lchild, *rchild; /* 左右孩子指针 */
} BiTNode, *BiTree;
5.1.1 插入算法
插入新节点的过程
- 若二叉排序树T为空,则为待插入的关键字key申请一个新结点,并令其为根;
- 若二叉排序树T不为空,则将key和根的关键字比较:
-若二者相等,则说明树中已有此关键字key,无须插入。
-若key<T→key,则将key插入根的左子树中。
-若key>T→key,则将它插入根的右子树中。
子树中的插入过程与上述的树中插入过程相同。如此进行下去,直到将key作为一个新的叶结点的关键字插入到二叉排序树中,或者直到发现树中已有此关键字为止。
5.1.3 查找算法
查找步骤
- 若二叉树T为空树,则搜索失败,否则:
- 若查找的数x等于T根节点的数据域的值,则查找成功,否则:
- 若查找的数x小于T根节点的数据域的值,则搜索左子树,否则:
- 查找右子树
5.1.4 删除算法
删除步骤(分三种情况)
- 若p结点为叶子结点,即该节点左子树PL和右子树PR均为空树。由于删去叶子结点不破坏整棵树的结构,则只需修改其双亲结点的指针即可。
- 若p结点只有左子树PL或右子树PR,此时只要令PL或PR直接成为其双亲结点f的左子树(当p是左子树)或右子树(当p是右子树)即可,作此修改也不破坏二叉排序树的特性。
- 若p结点的左子树和右子树均不空。在删去p之后,为保持其它元素之间的相对位置不变,可按中序遍历保持有序进行调整。比较好的做法是,找到p的直接前驱(或直接后继)s,用s来替换结点p,然后再删除结点s。
5.2 平衡二叉树
AVL树(平衡二叉树)要求对于每一个节点来说,它的左右子树的高度之差不能超过1,如果插入或者删除一个节点使得高度之差大于1,就要进行节点之间的旋转,将二叉树重新维持在一个平衡状态。这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。
平衡二叉树实现的大部分过程和二叉查找树是一样的(学平衡二叉树之前一定要会二叉查找树),区别就在于插入和删除之后要写一个旋转算法去维持平衡,维持平衡需要借助一个节点高度的属性。
5.2.1 满足平衡二叉树的条件
- 一棵空树是平衡二叉树;
- 若 T 是一棵非空二叉树,其左、右子树为 TL 和 TR ,令 hl 和 hr 分别为左、右子树的深度。当且仅当
TL 、 TR 都是平衡二叉树;
| hl - hr |≤ 1;
时,则 T 是平衡二叉树。
相应地定义 hl - hr 为二叉平衡树的平衡因子 (balance factor) 。因此,平衡二叉树上所有结点的平衡因子可能是 -1,0 ,1 。换言之,若一棵二叉树上任一结点的平衡因子的绝对值都不大于 1 ,则该树是就平衡二叉树。
假设二叉排序树的最小不平衡子树的根结点为 A ,则调整该子树的规律可归纳为下列四种情况:
- LL型
新结点 X 插在 A 的左孩子的左子树里。调整方法见下图 (a) 。图中以 B 为轴心,将 A 结点从 B 的右上方转到 B 的右下侧,使 A 成为 B 的右孩子。 - RR型
新结点 X 插在 A 的右孩子的右子树里。调整方法见下图 (b) 。图中以 B 为轴心,将 A 结点从 B 的左上方转到 B 的左下侧,使 A 成为 B 的左孩子。 - LR型
新结点 X 插在 A 的左孩子的右子树里。调整方法见图 © 。分为两步进行:第一步以 X 为轴心,将 B 从 X 的左上方转到 X 的左下侧,使 B 成为 X 的左孩子, X 成为 A 的左孩子。第二步跟 LL 型一样处理 ( 应以 X 为轴心 ) 。 - RL型
新结点 X 插在 A 的右孩子的左子树里。调整方法见图 (d) 。分为两步进行:第一步以 X 为轴心,将 B 从 X 的右上方转到 X 的右下侧,使 B 成为 X 的右孩子, X 成为 A 的右孩子。第二步跟 RR 型一样处理 ( 应以 X 为轴心 ) 。
5.2.2平衡二叉树的创建
创建平衡二叉树,我们采用依次插入节点的方式进行。而平衡二叉树上插入节点采用递归的方式进行。递归算法如下:
-若该树为一空树,那么插入一个数据元素为e的新节点作为平衡二叉树的根节点,树的高度增加1。
-若待插入的数据元素e和平衡二叉树的根节点的关键字相等,那么就不需要进行插入操作。
-若待插入的元素e比平衡二叉树的根节点的关键字小,而且在平衡二叉树的左子树中也不存在和e有相同关键字的节点,则将e插入在平衡二叉树的左子树上,并且当插入之后的左子树深度增加1时,分别就下列情况处理之:
- 平衡二叉树的根节点的平衡因子为-1(右子树的深度大于左子树的深度):则将根节点的平衡因子更改为0,平衡二叉树的深度不变;
- 平衡二叉树的根节点的平衡因子为0(左右子树的深度相等):则将根节点的平衡因子修改为1,平衡二叉树的深度增加1;
- 平衡二叉树的根节点的平衡因子为1(左子树的深度大于右子树的深度):若平衡二叉树的左子树根节点的平衡因子为1,则需要进行单向右旋转平衡处理,并且在右旋处理后,将根节点和其右子树根节点的平衡因子更改为0,树的深度不变;
- 若平衡二叉树的左子树根节点的平衡因子为-1,则需进行先向左,后向右的双向旋转平衡处理,并且在旋转处理之后,修改根节点和其左,右子树根节点的平衡因子,树的深度不变;
-若e的关键字大于平衡二叉树的根节点的关键字,而且在平衡二叉树的右子树中不存在和e有相同关键字的节点,则将e插入到平衡二叉树的右子树上,并且当插入之后的右子树深度加1时,分别就不同的情况处理之:
- 平衡二叉树的根节点的平衡因子是1(左子树的深度大于右子树的深度):则将根节点的平衡因子修改为0,平衡二叉树的深度不变;
- 平衡二叉树的根节点的平衡因子是0(左右子树的深度相等):则将根节点的平衡因子修改为-1,树的深度加1;
- 平衡二叉树的根节点的平衡因子为-1(右子树的深度大于左子树的深度):若平衡二叉树的右子树根节点的平衡因子为1,则需要进行两次选择,第一次先向右旋转,再向左旋转处理,并且在旋转处理之后,修改根节点和其左,右子树根节点的平衡因子,树的深度不变;
- 若平衡二叉树的右子树根节点的平衡因子为1,则需要进行一次向左的旋转处理,并且在左旋之后,更新根节点和其左,右子树根节点的平衡因子,树的深度不变;
设一组记录的关键字按以下次序进行插入: 4 、 5 、 7 , 2 、 1 、 3 、 6 ,创建平衡二叉树。
5.3 红黑树
红黑树的介绍
红黑树,一种二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。
通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
红黑树节点的组成
红黑树上每个结点内含五个域,color,key,left,right,parent。如果相应的指针域没有,则设为NULL。
红黑树的性质
一般的,红黑树,满足以下性质,即只有满足以下全部性质的树,我们才称之为红黑树:
- 每个结点要么是红的,要么是黑的。
- 根结点是黑的。
- 每个叶结点,即空结点(NULL)是黑的。
- 如果一个结点是红的,那么它的俩个儿子都是黑的。
- 从根到叶节点的每条路径,必须包含相同数目的黑色节点。
红黑树的操作
当我们在对红黑树进行插入和删除等操作时,对树做了修改,那么可能会违背红黑树的性质。
为了保持红黑树的性质,我们可以通过对树进行旋转,即修改树种某些结点的颜色及指针结构,以达到对红黑树进行插入、删除结点等操作时,红黑树依然能保持它特有的性质(如上文所述的,五点性质)。
修正方式:
改变节点的颜色
旋转
6. 排序
排序中的关键操作
比较:任意两个数据元素通过比较操作确定先后次序。
交换:数据元素之间需要交换才能得到预期结果。
6.1 插入排序
插入排序算法是一种简单的排序算法,也成为直接插入排序算法。它是一种稳定的排序算法,对局部有序的数据具有较高的效率。
插入排序算法是一个队少量元素进行排序的有效算法。每一次从无序序列中拿出一个数据,将它放到已排序的序序列的正确位置,如此重复,直到所有的无序序列中的数据都找到了正确位置。
void insertSort(int arr[], int len)
{
for (int i = 1; i < len;i++)
{
if (arr[i-1] > arr[i])
{
int temp = arr[i];
int j = i - 1;
for (; j >= 0 && temp < arr[j]; j--)
{
//符合条件的元素数据后移
arr[j + 1] = arr[j];
}
arr[j + 1] = temp;
}
}
}
void printArray(int arr[] ,int len)
{
for (int i = 0; i < len; i++)
{
printf("%d\n",arr[i]);
}
}
void test01()
{
int arr[] = { 4, 1, 2, 3, 5, 7, 6 };
//插入排序
int len = sizeof(arr) / sizeof(int);
insertSort(arr, len);
//打印数组
printArray(arr, len);
}
6.2 冒泡排序
冒泡排序算法的运作如下:(从后往前)
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
int main()
{
//int array[1024]={0};
int n=0;
int array[10]={9,2,5,7,-1,0,8,31,48,29};
int i,j;
int temp;
//while(scanf("%d",array[])!=EOF)
//{
// scanf("%d",&n);
for(i=9;i>0;i--)
{
for(j=0;j<i;j++)
{
if(array[j]>array[j+1])
{
temp=array[j];
array[j]=array[j+1];
array[j+1]=temp;
}
}
}
printf("%d %d\n",array[0],array[9]);
array[0]=0;
array[9]=0;
for(i=9;i>0;i--)
{
for(j=0;j<i;j++)
{
if(array[j]>array[j+1])
{
temp=array[j];
array[j]=array[j+1];
array[j+1]=temp;
}
}
}
for(i=0;i<10;i++)
{
printf("%d ",array[i]);
}
//}
return 0;
}
6.3 选择法
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
设数组为a[0…n-1]。
- 初始时,数组全为无序区为a[0…n-1]。令i=0
- 在无序区a[i…n-1]中选取一个最小的元素,将其与a[i]交换。交换之后a[0…i]就形成了一个有序区。
- i++并重复第二步直到i==n-1。排序完成。
稳定性
选择排序是不稳定的排序方法
选择排序效率:O(n²)
6.4 快速排序
算法介绍
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。
分治法基本思想
- 先从数列中取出一个数作为基准数(枢轴)。
- 分区过程将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
- 再对左右区间重复第二步,直到各区间只有一个数。