做题技巧:
- 算法题,先在脑海里理清思路,再画出程序框图,最后依照程序框图写代码。基本操作的具体不用写,写出函数声明即可,注意用注释写出函数的作用。
第三章. 栈,队列和数组
1.栈
操作受限的线性表,只能在一端进行插入和删除
n个不用元素进栈,出栈元素的不同排列的个数为
1.1顺序栈
#include <iostream>
#define MaxSize 10
//顺序栈//
typedef struct{
ElemType data[MaxSize]; //存放栈中各个元素
int top; //指向栈顶元素
}SqStack;
//初始化
void InitStack(SqStack &S);
//判空
bool StackEmpty(SqStack S);
//新元素入栈
bool Push(SqStack &S, ElemType x);
//出栈操作
bool Pop(SqStack &S, ElemType &x);
//读取栈顶元素
bool GetTop(SqStack &S, ElemType &x);
int main(int argc, char** argv) {
return 0;
}
//初始化
void InitStack(SqStack &S){
S.top = -1; //初始化栈顶指针 top的值为-1标示为空栈,非空时top值为栈顶元素下标;(也可用别的方式标识,比如栈空时top=0,不空时top存放栈顶元素数组下标的下一个)
};
//判空
bool StackEmpty(SqStack S){ //顺序栈的实质是一个数组加上一个整型数,传进来的是复制品
if(S.top == -1)
return true;
return false;
};
//新元素入栈
bool Push(SqStack &S, ElemType x){
if(S.top==MaxSize-1)
return false;
S.top++; //指针先自增1
S.data[S.top] = x; //新元素入栈
//以上两句可以合并为 S.data[++S.top] = x;
return true;
};
//出栈操作
bool Pop(SqStack &S, ElemType &x){ //用x返回删除的元素
if(S.top == -1)
return false; //栈空,报错
x = S.data[S.top];
S.top--;
//上面两句可以合并成一句 x = S.data[S.top--]; (记 i++,和++i的区别)
return true;
};
//读取栈顶元素
bool GetTop(SqStack &S, ElemType &x){ //用&起到指针的效果吗?应该是的,不会再复制了,如果数据元素大,节省空间且速度更快
if(S.top == -1)
return false; //栈空,报错
x = S.data[S.top];
return true;
};
/*为了缓解顺序栈的缺点:栈的大小不可变,提高正片空间的利用率 --->共享栈 两个栈共享同一片内存空间。
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int top0; //0号栈 的栈顶指针
int top1; //1号栈 的栈顶指针
}ShStack;
//初始化栈
void InitStack(ShStack &S){
S.top0 = -1;
S.top2 = MaxSize;
};
注:共享栈
设置两个栈顶指针,共享同一片连续的存储空间(数组)。
1.2链栈
进栈和出栈都只能在栈顶一端进行(链头作为栈顶)--->即插入和删除只能在单链表表头进行
#include <iostream>
#include <stdlib.h>
typedef struct LStack{
int data; //每个节点存放的数据元素
struct LStack * next; //指针域,指向下一个节点
}LStack, *LinkStack;
//1.初始化
LinkStack Initlist(LinkStack &S);
//2.判空
bool Empty(LinkStack S);
//3.进栈
bool Push(LinkStack &S, int x);
//4.出栈
bool Pop(LinkStack &S, int &x); //一定要注意何时用&,想要改变真实的S的指向,就必须用
//5.获取栈顶元素
bool GetTop(LinkStack S, int &x);
//6.判断满 //链栈是动态分配的节点空间,不存在栈满的情况
//7.输出栈元素
bool Output(LinkStack S);
int main(int argc, char** argv) {
LinkStack S;
int x;
Initlist(S);
if(Empty(S))
printf("S空\n");
else
printf("S非空\n");
Push(S, 1);
Push(S, 2);
Push(S, 3);
Push(S, 4);
Output(S);
Pop(S, x);
printf("%d \n", x);
Output(S);
Pop(S, x);
printf("%d \n", x);
Output(S);
if(Empty(S))
printf("S空\n");
else
printf("S非空\n");
return 0;
}
//不带头初始化
LinkStack Initlist(LinkStack &S){ //再次注意什么时候可以不用&,什么时候必须用,为什么。(初始化必须用,操作不需要)
S = NULL;
return S;
};
//不带头 判空
bool Empty(LinkStack S){
if (S == NULL)
return true;
else
return false;
};
//3.进栈
bool Push(LinkStack &S, int x){
LinkStack p;
p = (LinkStack)malloc(sizeof(LStack));
p->data = x;
p->next = S;
S = p;
return true;
};
//4.出栈
bool Pop(LinkStack &S, int &x){
if(S == NULL)
return false;
LinkStack p;
p = S;
x = S->data;
S = S->next;
free(p);
return true;
};
//5.获取栈顶元素
bool GetTop(LinkStack S, int &x){ //这里x是引用类型,直接修改的真正的x,算是传出去了,更新了x
if(S != NULL){
x = S->data;
return true;
}
return false;
};
//7.输出栈元素
bool Output(LinkStack S){
if(S != NULL){
LinkStack p;
printf("%d",S->data);
p = S->next;
while(p->next != NULL){
printf("%d", p->data);
p = p->next;
}
printf("%d\n",p->data); //输出最后一个元素 或者while中用p!=NULL就不用要这句了(想想为啥);
return true;
}
return false;
};
2.队列
first in first out(FIFO)先进先出
操作受限的线性表,只能在一端进行插入,另一端进行删除;
基本操作:
- 初始化
- 销毁队列
- 入队
- 出队
- 读队头元素
2.1顺序存储
front == rear 标示队空,为了最大化利用空间--->循环队列;循环队列也是front == rear 标示队空
#include <iostream>
#define MaxSize 4
typedef struct{
int data[MaxSize]; //用静态数组存放队列元素
int front, rear; //队头指针和队尾指针 front指向队头元素,rear指向最后一个元素的下一个位置 (也可以有别的方法表示,比如front指向队头,rear指向队尾,理解思想,灵活运用)
}SqQueue;
//判空
bool QueueEmpty(SqQueue &Q);
//初始化队列
void InitQueue(SqQueue &Q);
//入队
bool EnQueue(SqQueue &Q, int x);
//出队
bool DeQueue(SqQueue &Q, int &x);
//获得队头元素的值,用x返回
bool GetHead(SqQueue &Q, int &x);
//输出队列元素
void OutPut(SqQueue &Q);
int main(int argc, char** argv) {
int x;
SqQueue Q;
InitQueue(Q);
if(QueueEmpty(Q))
printf("队空\n");
else
printf("队不空\n");
EnQueue(Q, 1);
EnQueue(Q, 2);
EnQueue(Q, 3);
OutPut(Q);
printf("\n");
DeQueue(Q, x);
OutPut(Q);
return 0;
}
//判空
bool QueueEmpty(SqQueue &Q){
if(Q.rear == Q.front) //队头和队尾指向一样表示队列为空
return true;
else
return false;
};
//初始化队列
void InitQueue(SqQueue &Q){
//初始时 队头队尾指向0
Q.rear = 0;
Q.front = 0;
};
//入队
bool EnQueue(SqQueue &Q, int x){
if((Q.rear+1)%MaxSize == Q.front) //队列已满 循环队列 "if里是队满的条件,不同处理,不一样,书上p79,三种处理方法"
return false;
Q.data[Q.rear] = x; //新元素插入队尾
Q.rear = (Q.rear+1) % MaxSize;
return true;
}
//出队
bool DeQueue(SqQueue &Q, int &x){
if(Q.front == Q.rear)
return false; //队空,报错
x = Q.data[Q.front];
Q.front = (Q.front+1)%MaxSize;
return true;
}
//获得队头元素的值,用x返回
bool GetHead(SqQueue &Q, int &x){
if(Q.rear = Q.front)
return false; //队空,报错
x = Q.data[Q.front];
return true;
};
//输出队列元素
void OutPut(SqQueue &Q){
if(Q.rear == Q.front)
printf("队空,没有数据元素");
else
for(int i=Q.front; i!=Q.rear;i++%MaxSize)
printf("%d ", Q.data[i]);
};
/*
队空 : front == rear
队满: (rear+1)%MaxSize == front
另法:增加size标志变量,0表示空 size == MaxSize表示队满 (队空,队满都满足front=rear)
用tag标志变量,0表示上次操作为删除,1表示上次操作为插入
队空:tag=0时,若因删除导致front == rear
队满:tag=1时,若因插入导致front == rear
*/
2.2链式存储
操作首先的单链表,只能在表头删,表尾插入;(先进先出)
#include <iostream>
#include <stdlib.h>
typedef struct LinkNode{ //链式队列节点
int data;
struct LinkNode *next;
}LinkNode;
typedef struct{ //链式队列 (模块化,直接用LinkQueue就可以声明一个队列)
LinkNode *front, *rear;//队列的队头和队尾指针
}LinkQueue;
//初始化(带头节点)
void InitQueue(LinkQueue &Q);
//判空(带头节点)
bool IsEmpty(LinkQueue Q);
//入队
void EnQueue(LinkQueue &Q, int x);
//出队
bool DeQueue(LinkQueue &Q, int &x);
//遍历
void OutPut(LinkQueue &Q);
int main(int argc, char** argv) {
int x;
LinkQueue Q;
InitQueue(Q);
OutPut(Q);
printf("\n");
EnQueue(Q, 1);
OutPut(Q);
printf("\n");
EnQueue(Q, 2);
OutPut(Q);
printf("\n");
DeQueue(Q, x);
OutPut(Q);
printf("\n");
DeQueue(Q, x);
OutPut(Q);
printf("\n");
return 0;
}
////初始化(带头节点)
//void InitQueue(LinkQueue &Q){
// //初始时front和rear都指向头结点;
// Q.front=Q.rear=(LinkNode*)malloc(sizeof(LinkNode));
// Q.front->next=NULL;
//};
////判空(带头节点)
//bool IsEmpty(LinkQueue Q){
// if(Q.front == Q.rear) //判断条件也可以为 Q.front->next ==NULL;(头结点指向空)
// return true;
// else
// return false;
//};
////入队
//void EnQueue(LinkQueue &Q, int x){
// LinkNode *p = (LinkNode*)malloc(sizeof(LinkNode));
// p->data = x;
// p->next = NULL;
// Q.rear->next = p; //新节点插入到rear之后
// Q.rear = p; //修改表尾指针
//
//}
////出队
//bool DeQueue(LinkQueue &Q, int &x){
// if(Q.front == Q.rear)
// return false; //空队
// LinkNode *p;
// p = Q.front->next;
// x = p->data; //用x返回队头元素
// Q.front->next=Q.front->next->next; //修改头结点的next指针
// if(Q.rear == p) //此次是最后一个节点出队
// Q.rear = Q.front; //修改尾指针
// free(p); //释放空间
// return true;
//}
////遍历
//void OutPut(LinkQueue &Q){
// LinkNode *p;
// if(Q.front == Q.rear)
// printf("队列为空");
// p = Q.front->next;
// while(p !=NULL){
// printf("%d ",p->data);
// p = p->next;
// }
//}
//初始化(不带头节点)
void InitQueue(LinkQueue &Q){
//初始时 front和rear都指向空
Q.front=NULL;
Q.rear=NULL;
};
//判空(不带头节点)
bool IsEmpty(LinkQueue Q){
if(Q.front == NULL) //判断条件也可以为 Q.rear ==NULL;(尾结点指向空)
return true;
else
return false;
};
//入队(不带头结点的队列,第一个元素入队时需要特殊处理)
void EnQueue(LinkQueue &Q, int x){
LinkNode *p = (LinkNode*)malloc(sizeof(LinkNode));
p->data = x;
p->next = NULL;
if(Q.front == NULL){ //在队列中插入第一个元素
Q.front = p; //修改队头队尾指针
Q.rear = p;
}
else{
Q.rear->next = p; //新节点插入到rear之后
Q.rear = p; //修改表尾指针
}
}
//出队
bool DeQueue(LinkQueue &Q, int &x){
if(Q.front == NULL)
return false; //队空
LinkNode *p;
p = Q.front;
x = p->data;
Q.front = p->next;
if(Q.rear == p)
Q.rear = NULL;
free(p);
}
//遍历
void OutPut(LinkQueue &Q){
LinkNode *p;
if(Q.front == NULL)
printf("队列为空");
p = Q.front;
while(p !=NULL){
printf("%d ",p->data);
p = p->next;
}
}
2.3双端队列
考点:判断输出序列合法性,不考基本操作,代码等
自命题可能不考,略微看了下
3.栈和队列的应用
3.1栈的应用
3.1栈在括号匹配中的应用
流程图:
#include <iostream>
#include <string.h>
#define MaxSize 10
//顺序栈//
typedef struct{
char data[MaxSize]; //存放栈中各个元素
int top; //指向栈顶元素
}SqStack;
//初始化
void InitStack(SqStack &S);
//判空
bool StackEmpty(SqStack S);
//新元素入栈
bool Push(SqStack &S, char x);
//出栈操作
bool Pop(SqStack &S, char &x);
//读取栈顶元素
bool GetTop(SqStack &S, char &x);
//括号匹配
bool BracketMatch(char str[], int length){
SqStack S;
InitStack(S);
for(int i=0;i<length;i++){
if(str[i]=='(' || str[i]=='[' || str[i]=='{') //扫描到左括号,进栈
Push(S, str[i]);
else{
if(StackEmpty(S)) //扫描到右括号,但是栈空 ,匹配失败
return false;
char topElem;
Pop(S, topElem);//栈顶出栈,如果不匹配,匹配失败
if(str[i]==')'&&topElem!='(')
return false;
if(str[i]==']'&&topElem!='[')
return false;
if(str[i]=='}'&&topElem!='{')
return false;
}
}
return StackEmpty(S);
};
int main(int argc, char** argv) {
char str[8];
int length;
printf("请输入括号串:");
gets(str);
length = strlen(str);
if(BracketMatch(str, length))
printf("匹配成功!\n");
else
printf("匹配失败....\n");
return 0;
}
//初始化
void InitStack(SqStack &S){
S.top = -1; //初始化栈顶指针 top的值为-1标示为空栈,非空时top值为栈顶元素下标;(也可用别的方式标识,比如栈空时top=0,不空时top存放栈顶元素数组下标的下一个)
};
//判空
bool StackEmpty(SqStack S){ //顺序栈的实质是一个数组加上一个整型数,传进来的是复制品
if(S.top == -1)
return true;
return false;
};
//新元素入栈
bool Push(SqStack &S, char x){
if(S.top==MaxSize-1)
return false;
S.top++; //指针先自增1
S.data[S.top] = x; //新元素入栈
//以上两句可以合并为 S.data[++S.top] = x;
return true;
};
//出栈操作
bool Pop(SqStack &S, char &x){ //用x返回删除的元素
if(S.top == -1)
return false; //栈空,报错
x = S.data[S.top];
S.top--;
//上面两句可以合并成一句 x = S.data[S.top--]; (记 i++,和++i的区别)
return true;
};
//读取栈顶元素
bool GetTop(SqStack &S, char &x){ //用&起到指针的效果吗?应该是的,不会再复制了,如果数据元素大,节省空间且速度更快
if(S.top == -1)
return false; //栈空,报错
x = S.data[S.top];
return true;
};
3.2栈-表达式求值问题
-
中缀表达式: 运算符在两个操作数中间。如:1+2+3/2
-
后缀(逆波兰)表达式: 运算符在两个操作数之后。
如:中缀(a+b)/c 转化为后缀:ab+c/
-
前缀(波兰)表达式: 运算符在两个操作数之前
如:中缀(a+b)/c 转化为前缀:/+abc
因为运算优先级的问题,同级可以按不同的先后顺序算,因此中缀转后缀、前缀,结果不唯一
为了使同一个表达式转化为后缀或者前缀结果唯一,便于计算机操作
作如下规定:
中缀转后缀:左优先原则 中缀转前缀:右优先原则
1>.中缀转后缀(左优先原则):
栈里存放的是还不能确定生效顺序的运算符或者界限符
//中缀表达式转换为后缀表达式
#include <iostream>
#include <stdlib.h>
#include <string.h>
typedef struct LStack{
char data; //每个节点存放的数据元素
struct LStack * next; //指针域,指向下一个节点
}LStack, *LinkStack;
//1.初始化
LinkStack Initlist(LinkStack &S);
//2.判空
bool Empty(LinkStack S);
//3.进栈
bool Push(LinkStack &S, char x);
//4.出栈
bool Pop(LinkStack &S, char &x); //一定要注意何时用&,想要改变真实的S的指向,就必须用
//5.获取栈顶元素
bool GetTop(LinkStack S, char &x);
//6.判断满 //链栈是动态分配的节点空间,不存在栈满的情况
//7.输出栈元素
bool Output(LinkStack S);
//中缀转后缀
void toSuffix(char str[], char str2[], int length){
LinkStack S;
Initlist(S);
int j=0;
char x=0;
for(int i=0; i<length; i++){
if(str[i] == '(')
Push(S, str[i]);
else if(str[i] == ')'){
GetTop(S, x);
while(x != '('){
Pop(S, x);
str2[j++] = x;
GetTop(S, x);
}
Pop(S, x);
}
else if(str[i]=='+' || str[i]=='-'){
GetTop(S, x);
while(x!='(' && !Empty(S)){
Pop(S, x);
str2[j++] = x;
GetTop(S, x);
}
Push(S, str[i]);
}
else if(str[i]=='*' || str[i]=='/'){
GetTop(S, x);
while((x=='*' || x=='/' ) && !Empty(S) && x!='('){
Pop(S, x);
str2[j++] = x;
GetTop(S, x);
}
Push(S, str[i]);
}
else
str2[j++]=str[i];
}
if(S != NULL){
LinkStack p;
str2[j++]=S->data;
p = S->next;
while(p!= NULL){
str2[j++] = p->data;
p = p->next;
}
}
};
int main(int argc, char** argv) {
char str[40];
int length;
printf("请输入表达式:");
gets(str);
printf("\n");
length = strlen(str);
char str2[length];
toSuffix(str, str2, length);
puts(str2);
//LinkStack S;
//Initlist(S);
//if(!Empty(S))
// printf("牛逼");
//else
// printf("垃圾");
return 0;
}
//不带头初始化
LinkStack Initlist(LinkStack &S){ //再次注意什么时候可以不用&,什么时候必须用,为什么。(初始化必须用,操作不需要)
S = NULL;
return S;
};
//不带头 判空
bool Empty(LinkStack S){
if (S == NULL)
return true;
else
return false;
};
//3.进栈
bool Push(LinkStack &S, char x){
LinkStack p;
p = (LinkStack)malloc(sizeof(LStack));
p->data = x;
p->next = S;
S = p;
return true;
};
//4.出栈
bool Pop(LinkStack &S, char &x){
if(S == NULL)
return false;
LinkStack p;
p = S;
x = S->data;
S = S->next;
free(p);
return true;
};
//5.获取栈顶元素
bool GetTop(LinkStack S, char &x){ //这里x是引用类型,直接修改的真正的x,算是传出去了,更新了x
if(S != NULL){
x = S->data;
return true;
}
return false;
};
//7.输出栈元素
bool Output(LinkStack S){
if(S != NULL){
LinkStack p;
printf("%c",S->data);
p = S->next;
while(p->next != NULL){
printf("%c", p->data);
p = p->next;
}
printf("%c\n",p->data); //输出最后一个元素 或者while中用p!=NULL就不用要这句了(想想为啥);
return true;
}
return false;
};
考点:转换到某一步时,栈里的情况是什么样的
//用栈进行后缀表达式的计算
#include <iostream>
#include <stdlib.h>
#include <string.h>
typedef struct LStack{
int data; //每个节点存放的数据元素
struct LStack * next; //指针域,指向下一个节点
}LStack, *LinkStack;
//1.初始化
LinkStack Initlist(LinkStack &S);
//2.判空
bool Empty(LinkStack S);
//3.进栈
bool Push(LinkStack &S, int x);
//4.出栈
bool Pop(LinkStack &S, int &x); //一定要注意何时用&,想要改变真实的S的指向,就必须用
//5.获取栈顶元素
bool GetTop(LinkStack S, int &x);
//6.判断满 //链栈是动态分配的节点空间,不存在栈满的情况
//7.输出栈元素
bool Output(LinkStack S);
//后缀表达式的计算
double Calculate(char str[]){
LinkStack S;
Initlist(S);
int x, y, s;
for(int i=0; i<strlen(str);i++){
switch (str[i]){
case '+':
Pop(S, y); //先出栈的是右操作数
Pop(S, x);
Push(S, x+y);
break;
case '-':
Pop(S, y);
Pop(S, x);
Push(S, x-y);
break;
case '*':
Pop(S, y);
Pop(S, x);
Push(S, x*y);
break;
case '/':
Pop(S, y);
Pop(S, x);
Push(S, x/y);
break;
default:
Push(S, str[i]-'0'); //字符型转换为相应的整型数
}
}
Pop(S, s);
return s;
}
int main(int argc, char** argv) {
int a;
char str[20];
gets(str);
a= Calculate(str);
printf("%d",a);
return 0;
}
//不带头初始化
LinkStack Initlist(LinkStack &S){ //再次注意什么时候可以不用&,什么时候必须用,为什么。(初始化必须用,操作不需要)
S = NULL;
return S;
};
//不带头 判空
bool Empty(LinkStack S){
if (S == NULL)
return true;
else
return false;
};
//3.进栈
bool Push(LinkStack &S, int x){
LinkStack p;
p = (LinkStack)malloc(sizeof(LStack));
p->data = x;
p->next = S;
S = p;
return true;
};
//4.出栈
bool Pop(LinkStack &S, int &x){
if(S == NULL)
return false;
LinkStack p;
p = S;
x = S->data;
S = S->next;
free(p);
return true;
};
//5.获取栈顶元素
bool GetTop(LinkStack S, int &x){ //这里x是引用类型,直接修改的真正的x,算是传出去了,更新了x
if(S != NULL){
x = S->data;
return true;
}
return false;
};
//7.输出栈元素
bool Output(LinkStack S){
if(S != NULL){
LinkStack p;
printf("%d",S->data);
p = S->next;
while(p->next != NULL){
printf("%d", p->data);
p = p->next;
}
printf("%d\n",p->data); //输出最后一个元素 或者while中用p!=NULL就不用要这句了(想想为啥);
return true;
}
return false;
};
先出栈的是“右操作数”
思考:后缀表达式怎么转中缀表达式
(拓展小知识:后缀表达式适用于基于栈的编程语言(stack-oriented programming language),如Forth,POSTScript)
2>.中缀转前缀(右优先原则)
基本不考,后缀更重要,看上面的后缀
//中缀表达式转化为前缀表达式
//用栈进行前缀表达式的计算
先出栈的是左操作数
总结:
3.3栈的应用-递归
本质:函数自己调用自己
函数调用的特点:最后被调用的函数最先执行结束(LIFO)---栈的特点
有函数的调用系统就会分配一个函数调用栈
例子:
缺点:如果递归层数太多的话,有可能会导致栈溢出
可能包含很多重复的计算(如eg2,Fib(1)算了很多次)
故递归越多,空间复杂度越大
总结:
4.迷宫求解
(22条消息) 迷宫求解【穷举求解法】_问路1的博客-CSDN博客_迷宫求解 ..
5.进制转换
具体思路:可能学过编程的娃都知道,十进制转二进制就是不断的给它取余,再取余,然后倒着写出来就行了。那么这个过程是不是正好满足栈先进后出的特点呐。嘿嘿~+~
(22条消息) 栈的应用--进制转换_Tattoo_Welkin的博客-CSDN博客_栈进制转换
常用进制转换方法(取商留余)原理解析, 附基于栈实现进制转换的代码 - 腾讯云开发者社区-腾讯云 (tencent.com)
3.4队列的应用
1>.树的层次遍历
先遍历1--1入队
---队头为1,1的左右孩子依次入队,1出队
---队头为2,2的左右孩子依次入队,2出队
---队头为3,3的左右孩子依次入队,3出队
,,,,,直至遍历完毕
2>.图的广度优先遍历
先遍历1--1入队
---队头为1,遍历1的相邻且未被遍历过的节点2、3入队,1出队
---队头为2,遍历2的相邻且未被遍历过的节点4入队,2出队
---队头为3,遍历3的相邻且未被遍历过的节点5、6入队,3出队
、、、、、、直至吧遍历结束
3>.在操作系统中的应用
[页面置换算法详解 - Leophen - 博客园 (cnblogs.com)](https://www.cnblogs.com/Leophen/p/11397699.html#:~:text=二、常见的页面置换算法 1 、FIFO(先进先出算法) 2 、OPT(最佳置换算法) 3,、LRU(最近最少使用算法) 4 、Clock(时钟置换算法) 5 、LFU(最不常用算法) 6 、MFU(最常使用算法))
先来先服务,符合队列的特点(先进先出FIFO) 这是页面置换算法
多个任务公用同一个设备或服务,用队列是一种实现方法
4.数组和特殊矩阵
🔴映射 矩阵的元素通过映射函数,映射到一维数组的元素
另:一维数组的某个元素映射到矩阵的哪个元素
1>.一维数组的存储结构:
逻辑上连续的元素映射到物理内存中
2>.二维数组的存储结构:
逻辑上连续的元素映射到物理内存中:两种策略,
- 行优先存储;
- 列优先存储;
3>.矩阵的存储
1.普通矩阵用二维数组存储
2.特殊矩阵可以压缩存储空间
用一维数组存储要存储的元素-----通过映射函数,把矩阵的元素映射到一维数组存储的元素,
另:一维数组的某个元素映射到矩阵的哪个元素
2.1对称矩阵
可以只存储主对角线和下三角区域(或主对角线和下三角区域)
🔴注意数组下标是从0开始的还是从1开始的
还要注意是按行优先存储还是列有限存储的
2.2三角矩阵
按行优先存储或者列优先存储用一维数组存储,个数为主对角线加下三角或上三角区域的元素,另外再多一个元素村常数
2.3三对角矩阵的压缩存储
2.4稀疏矩阵
三元组存储
十字链表存储
第四章.串
1.串的定义和实现
1>.字符编码
如ASCII编码,每个字符用八个二进制数表示
乱码问题就是编码错误造成的
2>.串的存储结构
1.顺序存储
注意字符的位序和其数组下标的不同
方案一:位序和数组下标差了1
方案二:char类型只能存0-255的数,存储串的长度有限
方案三:想知道串的长度只能从头到尾遍历,直到到\0
方案四:兼容方案一二 的优点,最好用
2.链式存储
每个节点存多个字符的时候,当最后一个节点没有装满的时候,可以用某些特殊的字符填充
结合链表,顺序表的优缺点,串的两种存储方式的优缺点
3>.串的基本操作
串的大小比较,就像是顺序的英文单词书,在前面的小
如:abc>aba abc>ab
⭐ 结合下面的存储结构,考虑如何实现上面的基本操作
结合上面两个操作(暴力的模式匹配,下面的KMP算法也是实现这个的)
总结:
2.串的模式匹配
研究子串
1>.朴素模式匹配
基本就是上一节的定位操作,合并了一下
若模式串长度为m, 主串长度为n
匹配成功的组好时间复杂度为O(m)
匹配失败的最坏时间复杂度为O(m(n-m+1))
**长度为n的主串有n-m+1个长度为m的子串
2>.牛逼的KMP算法
主串指针不回溯
原理是假如匹配到某个位置,则前面匹配过的一定是一样的
理解next数组就会了(最大的前缀和后缀一样)
🌟 注意:
next[1] = 0; 因为当模式串第一个不匹配时,主串模式串的游标都要+1,这么做是为了不让模式串的第一个字符特殊化
next[2] = 1; 如果当模式串的第二个字符不匹配时,前面只有1个字符,没有前后缀,因此最长相等先后缀长度为0,直 接从头开始比较(或者理解为公式中的+1)
next[i] 中,i是子串中前 i-1 位(匹配到第i位匹配失败了,看前面的子串的最长相同前后缀)的字符组成的串,前后缀的最大相同串长度,不是数组下标,记住啦
3>.next[]数组 和 nextval[]数组
nextval是改进的next数组
🌟求出next数组,从左往右依次确定nextval数组 :如果模式串的某个字符等于该字符的next数组对应的字符,将该字符的next数组更新为其next数组对应的字符的next数组
第五章. 树
1.树的基本概念
除了根节点外,任何一个节点有且仅有一个前驱(否则叫做网或图)
1>.基本术语:
2>.有序树、无序树:
3>.常用性质
-
节点数 = 总度数+1 每个节点都有1个边连着,而总度数等于边的条数,只有根节点没有
-
度为m的数、m叉树的区别
-
度为m的树第i层最多有个节点(第一层1个,即m的0次方)
-
高度为h的m叉树最多有个节点(m0+m1+···+m^h-1 等比数列求和)
-
高度为h的m叉树至少有h个节点
高度为h、度为m的树至少有h+m-1个节点
-
(用性质4推)
2.二叉树的概念
1>.基本概念
二叉树是一种递归定义的数据结构
二叉树的五种状态:
- 空二叉树
- 只有左子树
- 只有右子树
- 只有根节点
- 左右子树都有
2>.几个特殊的二叉树
1.满二叉树
2.⭐完全二叉树
🌟对于完全二叉树,如果某节点只有一个孩子,那么一定是左孩子
3.二叉排序树
下面第五章第五节详细讲
4.平衡二叉树
🌟如果一个二叉排序树是平衡的(平衡二叉树),那么该二叉排序树有更高的搜素效率
3>.二叉树的性质
1.N0 = N2 +1
2.具有n个节点的完全二叉树的高度h为
3.n个节点的完全二叉树的N0,N1,N2
4.二叉树的存储结构
1>.顺序存储
是按照层序遍历的顺序来存储的
(1)完全二叉树
结构体数组来存放,isEmpty来判断节点是否为空
用二叉树的的性质来找相应节点的左右孩子,父节点······
(2)普通二叉树
综上,顺序存储只适合存储完全二叉树,一般二叉树太浪费空间一般用链式存储来存储二叉树
2>.链式存储
n个节点,2n个指针,除了根节点没指针指向,其他节点均有指针指向,
故非空指针为 n-1 个,空指针为 2n-(n-1) = n+1 个(可以用来构建线索二叉树的线索)
三叉链表
--------用来找父节点(一般不用这个,用线索二叉树)
3.二叉树的遍历和线索二叉树
1>.二叉树的遍历
🌟 先/中/后序遍历,都是基于树的递归特性确定的次序规则
算术表达式的分析树:
(22条消息) 数据结构与算法——24. 树的应用:表达式解析树_花_城的博客-CSDN博客_解析树
1.先序遍历
递归算法:
递归过程:
一定要自己试走一遍
空间复杂度:O(h+1) 即 O(h)
h为二叉树的高度,(函数调用栈里最多压入h+1个函数的信息,其中最后一个是最后一层的下一层(空的))
每个节点被访问三次
对于先序遍历: 第一次路过该节点访问该节点 左边画圈
对于中序遍历: 第二次路过该节点访问该节点 下边画圈
对于后序遍历: 第三次路过该节点访问该节点 右边画圈
2.中序遍历
递归算法:
3.后序遍历
递归算法:
应用:
4.层次遍历
如果是顺序存储的(顺序存储是按层序遍历存储的),可以直接按序访问顺序存储中数组的元素
入队的是指向节点的指针,这样就可以大大节省空间
5.复杂度分析
(22条消息) 二叉树的前序遍历、中序遍历、后序遍历、层序遍历的时间复杂度和空间复杂度_algsup的博客-CSDN博客_中序遍历时间复杂度
(22条消息) 二叉树多种遍历的时间复杂度和空间复杂度_chen270的博客-CSDN博客_二叉树遍历的时间复杂度
6.非递归算法
特殊的是后序遍历,注意一下是怎么实现的
(22条消息) 二叉树的非递归遍历算法_Second to none的博客-CSDN博客_二叉树非递归遍历算法
书上用的是visit函数访问节点,这里用printf直接输出了;
2>.用二叉树的遍历序列构造二叉树
结论:若只给出一棵二叉树的 前/中/后/层 序遍历中的一种,不能唯一确定一棵二叉树
且不要中序序列也不行;因为没有中序序列无法划分树的左右子树
(1).前序+中序
例子:
eg1:
eg2:
(2).后序+中序
例子:
(3).层序+中序
例子:
总结
3>.线索二叉树
二叉树的链式存储中有n+1个空指针域,把他们利用起来,构造线索二叉树
指向某种遍历序列的前驱后继(左孩子指针指向前驱,右孩子指针指向后继,另外再在节点内添加两个标志变量,记录两个指针域到底是线索还是指向孩子),这里的前驱后继不是二叉树某个节点的前驱或后继
线索化前ltag和rtag都设置成0
(1).中序线索二叉树
代码:
pre指针是全局变量
visit中的q永远不会是NULL,因为在遍历函数中只有当T!=NULL才执行visit函数
最后pre指针和q指针指向相同,要特殊处理pre,使最后一个节点的右孩子指针线索化,下面的字段详解↓↓
本质是中序遍历的代码,只是在中序遍历的过程中,用visit函数,对当前访问节点的两个指针域做了处理(线索化)
当前访问节点的前驱指针在当前访问时线索化,后继指针在访问下一个节点的时候线索化------>这导致了一个问题,最后一个节点右孩子指针是空,应该线索化,但是遍历只会遍历到最后一个节点,要想对最后一个节点的右孩子指针线索化,只能再遍历最后一个节点的下一个节点?这是不可能的,因此要对其处理只能用全局变量pre,执行完函数,全局变量pre指向最后一个节点,如果pre->rchild=NULL(本来就是空(最后一个节点的右指针肯定指向空否则肯定不是中序遍历序列的最后一个节点),这句可以不要,增强可读性),pre->rtag=1;
上面visit函数中的两个if语句分别进行前驱线索化,和后继线索化,第二个if里的pre!=NULL是处理刚开始时,pre还没初始化,为空,这时候判断pre->rchildNULL就没意义了,只有pre!=NULL,才能进一步判断pre->rchild是否NULL。
王道代码:
是把上面的代码中visit合并了;
(2).先序线索二叉树
代码:
遍历递归时,访问到D节点时,会对D的左孩子指针进行线索化,指向前驱B,但是visit函数执行完之后,递归遍历左子树的时候,左孩子指针已经在visit函数中线索化,然后就递归死循环了--->用tag标志变量看指针到底指向的是不是真正的孩子,是的话,才递归
这种情况只有先序线索化的时候才会出现 ⭐ 好好思考思考
王道代码:
(3).后序线索二叉树
代码:
总概:
4>.线索二叉树找前驱/后继
(1).中序线索二叉树
后继:
如果该节点的右指针被线索化了(rtag==1),那么右指针指向的就是该节点的后继节点
如果该节点的右指针域指向右孩子(rtag==0),那么按照中序遍历的规则(左根右),该节点的下一个节点就是右子树的按中序遍历访问的第一个节点--->最左下角的节点(不一定是叶节点)
🌟 有了对中序线索二叉树后继节点的访问,可以用非递归算法对二叉树进行中序遍历--->先找到中序遍历的第一个节点(根节点的最左下角的节点),然后一直访问该节点的后继。空间复杂度大大降低为 O(1)
前驱:
如果该节点的左指针被线索化了(ltag==1),那么右指针指向的就是该节点的前驱节点
如果该节点的右指针域指向左孩子孩子(ltag==0),那么按照中序遍历的规则(左根右),该节点的前一个节点就是左子树的按中序遍历访问的最后一个节点--->最右下角的节点(不一定是叶节点)
🌟 有了对中序线索二叉树前驱节点的访问,可以对中序线索二叉树进行逆向的中序遍历--->先找到中序遍历的最后一个节点(根节点的最右下角的节点),然后一直访问该节点的前驱。
(2).先序线索二叉树
后继:
如果该节点的右指针被线索化了(rtag==1),那么右指针指向的就是该节点的后继节点
如果该节点的右指针域指向右孩子(rtag==0),那么按照中序遍历的规则(根左右),该节点的下一个节点就是
1.若该节点有左子树,则该节点的先序后继为左子树的第一个被访问的节点---左孩子
2.若该节点没有左孩子,则该节点的先序后继为右子树的第一个被访问的节点---右孩子
🌟 有了对先序线索二叉树后继节点的访问,就可以用非递归的方法实现先序遍厉---->先找到先序遍历的第一个节点---根节点,然后一次找后继节点就好了
前驱:
如果能找到p的前驱(可以用三叉链表来实现)
④如果p是根节点,则没有先序前驱
(3).后序线索二叉树
前驱:
如果该节点的左指针被线索化了(ltag==1),那么左指针指向的就是该节点的前驱节点
如果该节点的左指针域指向左孩子(ltag==0),那么按照后序遍历的规则(左右根),
1.如果该节点有右孩子,该节点的前一个节点就是右子树的最后一个被访问的节点---右孩子(右子树的根)
2.如果该节点没有右孩子,因为ltag==0,一定有左孩子,该节点的前一个节点就是左子树的最后一个被访问的节点---左孩子
🌟 有了对后序线索二叉树前驱节点的访问,就可以用非递归的方法实现先序遍厉---->先找到后序遍历的最后一个节点---根节点,然后一次找前驱节点就好了
后继:
如果能找到p的前驱(可以用三叉链表来实现)
④如果p是根节点,则没有先序后继
总结:
多思考思考:
4.树、森林
1>.树的逻辑结构
树是一种递归定义的数据结构
2>.树的存储结构
(1).双亲表示法
⭐ 存储方式: 1.顺序存储
用一个结构数组存放树,每个节点有两个成员,一个是其双亲节点的数组下标
用一个整型数保存树的节点个数
🌟 数组0号位置固定存储根节点
基本操作
1.增加一个数据元素: 在数组中空白的地方插入一个元素,双亲位置域设置成其双亲节点的数组下标
2.删除一个数据元素: (方法二更好保证了树的所有节点存放在数组的前面连续的空间,便于操作)
方法一:把删除的节点双亲位置域设置成-1,表示没有双亲,又因为不是数组第一个元素,由此可以确定为空节点
方法二:把数组最后一个元素赋值到要删除的节点,覆盖掉它,再把数组最后一个元素删除(双亲位置域设置成-1)
上面的删除操作只能删除叶子节点,如果是非叶子节点还要依次删除该节点的孩子节点,这就需要从头遍历数组,找到该节点的孩子节点,全部删除(递归来实现);因此,这也反映出第二种方法删除更好(第一种删除的话,每次递归都要从头遍历整个数组,第二个方法每次递归只需要遍历数组的前树长个元素)
(2).孩子表示法
⭐ 存储方式:顺序存储+链式存储
顺序存储各个节点,每个节点中保存孩子链表头指针
CTNode来构成链表,来存放某个节点的所有孩子的下标
CTBox来存放树的每个节点,其中一个是数据域,一个是指针域:存放一个指向该节点所有孩子节点的下标组成的链表
CTree是声明一棵孩子表示法的树,n节点数,r根的位置,也可以固定根的位置为数组的第一个元素
基本操作:
增:增加节点
删:删除节点
查:查双亲,查孩子 查孩子容易,找双亲比较难,需要从头遍历数组
(3).⭐孩子兄弟表示法
存储方式: 链式存储
🚙 左指针指向第一个孩子,右指针指向右兄弟
⭐ 树和二叉树的相互转换
⭐森林和二叉树的相互转换
总结
3>.树的遍历
树是一种递归定义的数据结构
(1).先根遍历
树的先根遍历序列和这棵树相应二叉树的先序序列相同
上面的代码为伪代码,不同的存储结构代码不同
(2).后根遍历
树的后根遍历序列和这棵树相应二叉树的中序序列相同
(3).层序遍历
队列来实现
对树的层序遍历又称为广度优先遍历
而后根遍历和先根遍历又称为深度优先遍历
4>.森林的遍历
(1).先序遍历
效果等同于依次对各个树进行先根遍历
森林的先序遍历序列和这个森林相应二叉树的先序序列相同
(2).中序遍历
效果等同于依次对各个树进行后根遍历(之所以叫中序遍历序列,是因为遍历序列和对应二叉树的中序序列相同)
森林的中序遍历序列和这个森林相应二叉树的中序序列相同
5>.总结
树的先根遍历序列和这棵树相应二叉树的先序序列相同
树的后根遍历序列和这棵树相应二叉树的中序序列相同
森林的先序遍历序列和这个森林相应二叉树的先序序列相同
森林的中序遍历序列和这个森林相应二叉树的中序序列相同
森林的先序遍历效果等同于依次对各个树进行先根遍历
森林的中序遍历效果等同于依次对各个树进行后根遍历(之所以叫中序遍历序列,是因为遍历序列和对应二叉树的中序序列相同)
5.树与二叉树的应用
1>.二叉排序树(BST)
Binary Search Tree
上面二叉树有提到
(1).定义
进行中序遍历,可以得到一个递增的有序数列左<根<右(中序遍历:左根右)
(2).基本操作
1.查找
循环查找,小于根节点在左子树查找,大于根节点在右子树查找
查找成功,返回该节点的指针,查找失败会返回的指针为空
递归实现
递归实现最坏空间复杂度为O(h+1) 即 O(h) h为树的深度
2.插入
跟查找代码差不多,先是找到正确的位置,再在该位置插入数据元素
这里是递归代码
新插入的节点一定是叶子节点
树中存在相同关键字的节点,插入失败
思考非递归算法:
int BST_Insert(BSTree &T, int k) //其实这些数据元素不仅仅只能是int型,其他复杂的数据类型也可以,设置比较大小的规则
{
while(T!==NULL && T->key != k) //先查找key应该放入的位置
{
if(key < T->key)
T=T->lchild;
else if(key > T->key)
T = T->rchild;
else if(key == T->key)
return 0; //二叉排序树中有该节点,插入失败
}
T->key = key; //插入key
return 1;
}
3.二叉排序树的构造
不断插入新节点的过程
插入序列的第一个节点就是根节点
常考点:给出一个序列,构造一个二叉排序树
4.删除
①若删除的节点为叶子节点,则直接删除,不会破坏二叉排序树的特性
即:左子树节点值<根节点值<右子树节点值
②若删除的节点为非叶子节点又分为两种情况
Ⅰ.若要删除的节点只有左子树或者只有右子树,那直接让其左子树或者右子树代替其原来位置即可不会破坏二叉排 序树的特性 即:左子树节点值<根节点值<右子树节点值
Ⅱ.若要删除的节点既有左子树又有右子树,让要删除节点的直接后继(或者直接前驱)代替该节点(赋值),然后从二叉 排序树中删除这个直接后继(或直接前驱),这样就转换成了 第一种 或者 第二种的第一种 的情况。
(3).查找效率分析
1.查找长度
查找的时间复杂度等于查找长度
2.查找成功的平均查找长度(ASL)
第一层节点查找成功的查找长度为1
第二层节点查找成功的查找长度为2
第三层节点查找成功的查找长度为3
……
查找操作的最坏时间复杂度取决于该二叉排序树的高度,为O(h) ----->为了提高查找效率
----->扩大宽度,减少深度
---->平衡二叉树
3查找失败的平均查找长度(ASL)
查找失败只会发生在叶子节点
第二层节点查找失败的查找长度为1
第三层节点查找失败的查找长度为2
……(比较的次数)
总结:
2>.平衡二叉树
(1).定义
n个节点的平衡二叉树,最小高度就是完全二叉树的高度,最大高度为)O(log2 n)
高度为log2(n+1),
数据结构课本上有最大高度。
最小高度就是完全二叉树了。
设N是深度为h的平衡二叉树的最少结点数,对于 h >= 1,有 N = F(h + 2) - 1 成立,其中的F(n)为Fibonacci 数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ...
于是最大高度H为F(H + 2) - 1 <= n < F(H + 3) - 1
如果一个有n个节点的二叉排序树平衡,那么其高度h为,因此其查找的时间复杂度为O(h)即O(log2n)
节点的平衡因子 = 左子树高 - 右子树高
(2).插入新节点
从插入点往回找到第一个不平衡节点,调整以该节点为根的子树
(3).调整最小不平衡子树
假如A节点的平衡因子为1
且 左孩子的左右子树高度相同还和A的右子树高度相同(即左孩子平衡因子为0)(如果左孩子高度不同,则可能会是左孩子变成了最小不平衡子树),则在A左孩子的左子树插入新节点,导致左子树高度+1,便会导致A节点不平衡,平衡因子变为2,A为最小不平衡子树
1.LL
假定A是最小不平衡子数据
注意:为啥节点A的平衡因子为1,在左孩子的左侧插入才会导致不平衡呢?
1.假如A平衡因子为0,左右子树同高,插入一个节点不会导致不平衡
2.假如A的平衡因子为-1,左侧子树高度比右侧子树高度低1,再在左子树插入一个节点,高度就相同了,也不会导致 不平衡
这的HR写错了,是BR
灰色的方形的框表示子树,不是表示孩子!框框下的H表示该子树的高度
2.RR
3.LR
4.RL
5.总结
填个坑↑
看这个图就懂了↓
练习
6.查找效率问题
高度为h的平衡二叉树最少有节点数:Nh=N(h-1)+N(h-2)+1
1 :根节点
N(h-1):左子树 或 右子树(且要保证节点数最少)
N(h-2):右子树 或 左子树(且要保证节点数最少)
(4).总结
3>.哈夫曼树
必须保证一个字符的编码不是另一个字符的前缀,这种称为前缀码。
(1).带权路径长度
(2).哈夫曼树的定义
中间的两个为哈夫曼树
确定节点的哈夫曼树不唯一
(3).构造哈夫曼树
n个节点构造哈夫曼树需要合并n-1次-----》会生成n-1个非叶子节点(因此一棵哈夫曼树有n个叶子节点,n-1个非叶子)
(4).哈夫曼编码
引入:
编码例子:
对相同的叶子节点进行哈夫曼编码,哈夫曼树可能不同,但是树的带权路径长度一样
英文字母频次
第六章.图
多对多
1.图的基本概念
1>.定义
顶点集+边集
图不可以为空: 顶点集不可以为空,边集可以为空
2>.图逻辑结构的应用
-
地图
-
社交好友
-
……
3>.有向图,无向图
无向图中用圆括号"()"表示一条边,有向图中用尖括号"<>"表示一条弧
4>.简单图、多重图
考研中只考简单图 了解一下就行了
4.1>.完全图
(22条消息) 图论(2)完全图,顶点的度与度序列_罗古洞的女婿的博客-CSDN博客_完全图的特征值
5>.顶点的度、入度、出度
6>.顶点间关系的描述
- 路径
- 回路
- 简单路径
- 路径长度
- 点到点的距离
- 连通
- 强连通
7>.连通图、强连通图
连通图对无向图而言,任意两个顶点之间有 路径
强连通图对有向图而言,任意两个顶点之间有 路径
8>.子图、生成子图
子图:顶点和边都是原图的子集
生成子图:顶点和原图的顶点一样,也就是把原图去掉几条边
子图和生成子图首先得是图(边的两边必须是顶点,定义),再谈别的
有向图和无向图一样
9>.连通分量、强连通分量
1.连通分量是对 无向图 而言的
每个连通分量都是原图的子图,而且这些子图还是连通的
极大连通子图:子图必须是连通的,且包含尽可能多的顶点和边
连通分量:就是极大连通子图;
三个要点:
- 是子图
- 是极大的子图
- 极大的子图是连通的
2.强连通分量是对有向图而言的
三个要点:
- 是子图
- 是极大的子图
- 极大的子图是连通的
10>.生成树
三个要点:
- 包含全部顶点
- 是子图
- 是极小的子图(即n个顶点n-1条边(树是n个节点n-1条边) )
- 极小的子图是连通的
2.生成森林
11.1>.简单路径,简单回路
在路径序列中,顶点不重复出现的路径称为简单路径。
除了第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
11>.边的权值、带权图、带权路径长度
无向图和有向图都可
12>.几种特殊形态的图
1.无向完全图 2.有向完全图
范围是n个顶点的图的边的范围,完全图是边数最大
3.稀疏图、稠密图
稀疏图和稠密图是相对的,没有确切的界限
这里的值只是个参考(边数<顶点数*log顶点数)
4.树、有向树
森林中,每个子图是极小的,同时各个子图又是连通的
2.图的存储结构和基本操作
1>.存储结构
(1).邻接矩阵法
🌟 顺序存储的 : 一个一维数组(存放顶点数据) + 一个二维数组(存放边的集合)
矩阵的行为出发顶点,矩阵的列为目标顶点
1.不带权
行为起始节点,列为终端节点,如edge[0] [1], 0号节点到1号节点的边
2.带权
A[j] [j]可以用无穷也可以用,表示没有自己到自己的边
3.求度、入度、出度
4.性能分析
适合存储稠密图(存稀疏图会浪费大量的空间),另外无向图是个对称矩阵,可以进行压缩存储(一维数组存储,映射函数)
空间复杂度:O(n^2)
5.邻接矩阵法的性质
很重要啊(这条性质只适用于不带权的)
好好思考思考思考思考思考思考
(这里思考了一下,不行,还是得写一下,不然会忘的
因为不带权的图边矩阵元素只有0和1,假如顶点顶点a到n有路径(a-b-c-……-n),则a-b,b-c,c-d,……,-n都有路径(显示为A[i] [j]=1), 则An次方中A[i] [j]不为0,即表示有路径,数值大小为长度为n的路径的条数
可以这么理解,A中不为0的元素说明两个顶点之间有路径 都为1 长度为1
A² 中,不为0 的元素为两元素之间有路径,长度为2, 个数为非零数值 算了,好麻烦,看着下面的图推吧,需要每次加一次方更好理解
另外,实对称矩阵乘以实对称矩阵不一定是实对称矩阵 ,
实对称矩阵可相似对角化 A=Qt·Λ·Q,(Qt=Q-1),则A^n = Qt· Λ^n ·Q 也为实对称矩阵 推不出 A^n也是实对称矩阵
(AB)t = BtAt =BA BA不一定等于AB
6.总结
(2).邻接表法
和前面树的孩子表示法存储结构一样
🌟 顺序存储+链式存储 :.
1.无向图:
2.有向图:
链表只存储节点的出边
3.求度、入度、出度
- 度:直接遍历无向图该节点的边链表,边链表中节点的个数即为该节点的度
- 入度:很麻烦,需要遍历所有节点
- 出度:直接遍历有向图该节点的边链表,边链表中节点的个数即为该节点的出度
总结:
邻接链表表示方式不唯一,邻接矩阵表示方式唯一
邻接表VS邻接矩阵
(3).十字链表法
用于存储有向图 空间复杂度和邻接表一样(优于邻接矩阵法),又解决了邻接表的缺点(找节点的入度很难)
相当于是把邻接表给功能增强了,既有入边指针,也有出边指针
弧尾相同的下一跳弧:出边
弧头相同的下一跳弧:入边
上面图中O(v^2)是邻接矩阵的空间复杂度
**十字链表法只用于存储有向图**
(4).邻接多重表
代码写起来很难,一般不会考
只用于存放无向图
用于存储无向图 解决了邻接表法每条边都存放了两份数据,不便于删除操作的问题
这不相就当于把两个边节点弄到一起了
如:
删除AB之间的边
删除顶点E:(不仅要删除节点E,还要删除与E相连的所有边)
性能分析:
(5).对比总结
存储结构不唯一
2>.基本操作
基本操作对应存储结构,考研中最常考图的邻接矩阵和邻接表的存储结构,十字链表和邻接多重表较难,不太考
此处的基本操作基于邻接矩阵和邻接表两种存储结构
(1).判断图G是否存在边<x,y>或(x,y)
无向图
有向图
(2).列出图G中与节点x相邻的边
无向图:
有向图:
邻接表存储的有向图节点的入边的查找需要遍历所有的边,但是找入边的查找效率两种存储方式优劣应具体问题具体分析(当存储的是个稀疏图的话,|E|的数量级就很小了)
(3).插入新的节点
插入的新节点没有连任何边
邻接矩阵的时间复杂度之所以为O(1),是因为那些0是邻接矩阵初始化的时候完成的;
无向图:
有向图:
有空在看,没讲,不难
(4).删除节点
无向图:
邻接矩阵删除节点直接把行列置空即可,用某个数字标记为空,其他元素不动(如果移动的话会有很大的时间开销)
有向图:
(5).增加边
无向图:
邻接表插入边的时候,用头插法时间开销更小,O(1)
有向图:
有向图类似无向图
(6). 删除边
无向图:
有向图:
(7).找顶点相连的第一个节点
无向图:
有向图:
有向图找入边临界点不太考
(8).下一个邻接点
无向图:
讲的说邻接表的复杂度为O(1) (因为已知前一个顶点,直接找下一个(链表)就好了)我感觉传进来的是顶点编号,需要遍历找,懂的,不用管
(9).找某条边或者弧的权值
(10).总结
这些基本操作中,只要找图的第一个邻接点和下一个邻接点用的较多,用于图的遍历
3.图的遍历
1>.广度优先遍历 BFS
Breadth First Search
方法:辅助队列
这里讲的是无向图,有向图在这节后面,类似
也就是层序遍历
先看树的广度优先遍历(层序遍历)
再看图的广度优先遍历(队列的应用里有讲到)
搜索相邻的顶点的时候,有可能搜到已经访问过的顶点(解决办法,每个节点用一个标志变量标记一下是否访问过,另外用一个数组来标记)
1.代码实现:
用队列来实现
2.遍历序列的可变性
图由邻接矩阵存储,遍历序列唯一
图由邻接表存储,遍历序列不唯一
(原因是找某个顶点的相邻节点的顺序会因为存储结构的不同而不同)
3.优化BFS-->最终版
(1)以上代码有缺陷
如果是非连通图,则无法遍历玩所有节点
(2)改进
4.复杂度分析:
(1).空间复杂度
空间占用主要是辅助队列占用的空间
(2).时间复杂度
主要时间开销是找连通分量的顶点和各个边
1.邻接矩阵存储
O(v^2)+O(2E) = O(v^2)
2.邻接表存储
O(v)+O(2E) = O(v+E)
5.广度优先生成树
由遍历序列得出
图如果由邻接矩阵存储,广度优先生成树唯一
图如果由邻接表存储,广度优先生成树不唯一,取决于邻接表中节点的顺序
6.广度优先生成森林
上面的广度优先生成树是对于连通图来说的
广度优先生成森林则是对于非连通图来说的
7.有向图的BFS算法
总结:
2>.深度优先遍历 DFS
方法:递归
图的深度优先遍历类似于树的先根遍历(也属于深度优先遍历)
1.代码实现
2.优化DFS-->最终版
(1).缺陷
类似于广度优先遍历,如果图为非连通图
(2).改进
3.复杂度分析
(1).空间复杂度
主要来自于函数的递归调用
答题时未特殊说明答最坏的复杂度
(2).时间复杂度
4.求深度优先遍历序列训练
想不起来的话,看看视频,挺好的
图由邻接矩阵存储,遍历序列唯一
图由邻接表存储,遍历序列不唯一(同一个图,邻接表的存储不唯一)
(原因是找某个顶点的相邻节点的顺序会因为存储结构(某节点相连的节点顺序)的不同而不同)
如果邻接表存储时,链表节点的顺序由小到大,那么遍历序列和邻接矩阵存储的遍历序列一样
遍历一个圈住一个,(注意递归)
改: 练习
总结
5.深度优先生成树
左边的是原邻接表存储的生成树,右边是改过的(上一节练习中的)生成树
6.深度优先生成森林
类似广度优先
上面的深度优先生成树是对于连通图来说的
深度优先生成森林则是对于非连通图来说的
3>.总结:图的遍历和连通性
1.对于无向图
2.对于有向图
如
起始顶点为7,只需调用一次BFS/DFS函数
起始顶点为2,就需要多次
4.图的应用
1>.最小生成树
研究对象:带权连通无向图
(1).引入(定义)
带权连通图的边的最小权值和 的生成树
道路规划要求所有地方连通,且成本尽可能低
(2).定义
如果一个连通图本身就是一棵树,则其最小生成树就是它本身(树的生成树不会更简了,即不能再删去任何一条边了)
只要连通图才有生成树,非连通图只有生成森林
(3).求最小生成树算法
算法不太考,会手算基本可以了
1.Prim算法(普里姆)
1.从某个顶点开始构建生成树
2.每次把连到树上代价最小的节点纳入生成树
(肯定是一对多,就是树,因为每次插入的节点只会连到一个节点上(唯一的父节点),不会构成环)
同一个图的最小生成树可能有多个
2.kruskal算法(克鲁斯卡尔)
3.对比
4.算法实现概况
两个辅助数组:
isJoin[i]: i节点是否已经加入生成树
lowCost[i]: i节点加入树的最小代价(与生成树的节点直接相连的节点,未直接相连的话为无穷)
每加入一个节点都要更新两个数组
用一个三元组,存放每条边的信息,再用并查集来判断节点是否连通
4.1 并查集
主要处理不相交集合的合并问题
1.1基本操作
如果两个节点父节点相同,那这两个节点连通
5.总结:
2>.最短路径问题
单源最短路径,各个顶点间的最短路径
无权图可以看做是一种特殊的带权图,只是每条边的权值都为1
1.BFS(广度优先)算法
只能求无权图的最短路径
从广度优先算法改造而来
广度优先遍历图,可以生成广度优先生成树,每个节点到根节点距离最短(想想为啥深度优先不行)
改造其中的visit函数;
增加了一个path数组和d数组
d[i]: 存放源点到i号节点的最短路径长度
path[i]: 存放i号节点的直接前驱
广度优先生成树是高度最小的(对应最短路径)
2.Dijkstra算法
-----》解决BFS算法的弊端(只能找不带权的单源最短路径)-----》能找带权的和不带权的单源最短路径
理解:
直接与源点相连的节点:找其中最短的,该节点确定为从源点到该节点的最短路径,因为是直接与源点相连,又因为是最短的,那么不可能再找到通过别的节点与源点相连的更小路径(从源点出去肯定要通过与源点直接相连的节点)
不是直接与源点相连的:找到还未确定最短路径的离源点最近的节点,确定其为从源点到该节点的最短路径,并通过该节点更新其他未确定最短路径的节点,因为该节点是未确定最短路径的节点中离源点最近的,因此该节点不可能再通过这些没有确定最短路径的节点找到更短的路径,而且这些节点都已经通过确定最短路径的节点更新过,也就是说这个待确定的节点也已经通过确定最短路径的节点更新过了(即该节点的路径此时已经是通过 确定最短路径的节点 的最短路路径),因此此时该节点找不到从别的节点与源点相连的更短的路径。 看不懂按下边的步骤走一遍,然后再看,就懂了
步骤
1.初始化,用到三个数组
final[i]:标记i号节点是否已经找到最短路径
dist[i]: 存储此时源点到i节点的最短路径
path[i]: 存储最短路径上i节点的前驱节点
初始化时候,dist数组初始化:把从源点到该节点有路径的节点的dist数组初始化为路径长度,其余的初始化为无穷;path初始化:把从源点到该节点有路径的节点的dist数组初始化为其前驱(源点),其余的初始化为-1;并把final中能够源点的对于的值改为TRUE,表示源点到源点已经找到最短路径,为0,其余的初始化为FALSE,表示该节点还未确定最短路路径;
2.找到未确定最短路径的节点中路径最短的节点,该节点确定最短路径,更新三个数组
3.循环同样的操作,直至全部节点处理完(final中全为TRUE)
.........
如何使用数组
代码,复杂度分析:
邻接矩阵 邻接表存储
缺陷:无法处理带负权值的图
3.Floyd算法
弗洛伊德算法(动态规划算法)
动态规划
步骤
第一层for是对k个节点,通过某个节点中转,进行k次迭代
后两层for是遍历矩阵,对矩阵中的元素值进行更新
如何通过两个数组找路径
代码,复杂度分析
可以解决Dijkstra算法解决不了的带负权值的图的最短路径问题
但是解决不了带负权回路的图的最短路径,这种图可能没有最短路径
总结:
3>.有向无环图 DAG图
(1).应用1.描述表达式
合并表达式:就是有向无环图
每个操作符都要有两个分支
合并规律
1.顶点中不肯出现重复的操作数
2.解题方法
3.合并可以合并的操作符
合并后每层最少有一个运算符
(2).应用2.拓扑排序
找到做事情的先后顺序
注意:无环图的拓扑排序可能不止一种,有环图不存在拓扑排序。
aov网有一个或多个拓扑排序序列---->做蛋炒饭顺序有多种
步骤:
若有环,则无法进行拓扑排序
代码
复杂度分析
逆拓扑排序
代码:
逆邻接表:链表保存的是指向自己的节点(入度边)
深度优先算法实现逆拓扑排序
深度优先算法实现拓扑排序在书上,会在综合题的第九题遇到
稍加修改:访问完
4>.关键路径
AOE网 :也是有向无环图
工程最短多久可以完成:源点到汇点的最长路径
求关键路径步骤
特性
总结
求最长路径
1。 肯定不能用dijkstra算法,这是因为,Dijkstra算法的大致思想是每次选择距离源点最近的结点加入,然后更新其它结点到源点的距离,直到所有点都被加入为止。当每次选择最短的路改为每次选择最长路的时候,出现了一个问题,那就是不能保证现在加入的结点以后是否会被更新而使得到源点的距离变得更长,而这个点一旦被选中将不再会被更新。例如这次加入结点u,最长路为10,下次有可能加入一个结点v,使得u通过v到源点的距离大于10,但由于u在之前已经被加入到集合中,无法再更新,导致结果是不正确的。
如果取反用dijkstra求最短路径呢,记住,dijkstra不能计算有负边的情况。。。
2.可以用 Bellman-Ford 算法求最长路径,只要把图中的边权改为原来的相反数即可。也可以用Floyd-Warshall 算法求每对节点之间的最长路经,因为最长路径也满足最优子结构性质,而Floyd算法的实质就是动态规划。但是,如果图中含有回路,Floyd算法并不能判断出其中含有回路,且会求出一个错误的解;而Bellman-Ford算法则可以判断出图中是否含有回路。
3.如果是有向无环图,先拓扑排序,再用动态规划求解。
第七章.查找
6.查找 查找的基本概念与术语;
静态查找表(顺序查找、折半查找、分块查找);
动态查找表(二叉排序树、二叉平衡树和 B-树); B-树 即 B树(-是横杆)
哈希表(哈希表的概念、 常用的哈希函数、解决冲突的方法);
查找算法的分析(ASL)及应用
1.基本概念
1.1基本概念
-
查找:
-
查找表:
-
关键字:
1.2查找表的基本操作
- 查找
- 插入删除
1.3查找算法的评价指标
-------->平均查找长度ASL
2.静态查找表
静态查找表:对查找表的操作只有 查,不能增删改 -----> 查找表不会改变
静态查找表只关注查找的速度
2.1顺序查找
- 又叫线性查找,从头到尾,或者从尾到头依次查找,常用于线性表
- ASL:O(n) (优化后也为O(n),不会有质的飞跃)
1.算法思想:
2.算法实现
哨兵:把要查找的元素的关键字放大茶渣宝数组的0号位置
查找成功,则返回元素下标;查找失败,则返回0
效率更高一些,但是基本没提升,只是少了一个条件判断
3.查找效率分析
4.优化
i、查找表里的各个数据元素被查找的概率相同
查找表里的元素有序存放
ii、查找表里的各个元素被查找的概率不同
把概率高的放的靠前----->只能提高查找成功的ASL
2.2折半查找
- 折半查找,又称“二分查找” ,仅适用于有序的顺序表(因为链表没有随机存取的特性,由low和high找到mid需要随机存取)。
- low high mid 为数组下标
-
- mid > key : high == mid-1;
- mid<key : low == mid +1;
- mid = key : 查找成功;
- low > high :查找失败;
1.算法思想
low > high的时候,查找失败
2.算法实现
low = mid +1;
high = mid-1;
3.查找效率分析
- 查找成功ASL:
- 查找失败ASL:
l.查找判定树的构造
二分查找的判定树一定是个 平衡二叉树
4.拓展
2.3分块查找
1.算法思想
块内无序,块间有序
先查索引,再查分块:
B+树类似多级分块查找
i. 用折半查找索引
2.查找效率分析
i.如果分块平均
3.拓展
上面的缺点:插入删除不方便,需要有大量的元素移动
解决方式:块内用顺序表存储----》改为用链式存储
3.动态查找表
动态查找表:对查找表的操作不仅有查, 还有增删改 ---- >查找表会改变
动态查找表不仅关注查找的速度,还要考虑插入删除操作是否方便实现
3.1二叉排序树
又叫二叉查找树
3.2二叉平衡树
5.5.2节
3.3 B树
B树的性质,插入、删除
1.二叉排序树-->n叉排序树
二叉排序树:
--->五叉排序树:
节点内关键字有序
2.保证n叉排序树的效率
-----》降低排序树的高度
i.策略1:保证查找树够宽
为什么除了根节点之外呢?
ii. 策略2:保证排序树不太高(平衡)
类似二叉平衡树(每个节点的子树高度差不超过1),不过更粗暴,所有节点的所有子树高度必须相同
3.B树
B树的B可以理解为Balance,平衡
3.1引入
策略1: m叉查找树中,规定除了根节点外,任何结点至少有「m/2|个分叉,即至少含有「m/2|- 1个关键字
策略: m叉查找树中,规定对于任何一个结点,其所有子树的高度都要相同。
上面的五叉排序树通过这两个条件的限制--->五阶B树
3.2定义,概念
注意:n在B树的插入和删除时候,用处很大
m阶B树的核心特性:
B树的高度:
注意:大多数学校计算B树的高度,不算叶子结点(失败节点)
4.B树插入、删除
i.插入
插入25;38;49;60 :
插入80:
- 插入88;90:
- 插入99:
- 插入83;87:
- 插入70:
- 插入92;93;94:
- 插入73;74;75:
总结:
新元素的插入,一定是插入到终端节点;
ii.删除
- 删除60:
- 删除80:
若被删除关键字在非终端节点,则用直接前驱或直接后继来替代被删除的关键字
直接前驱: 当前关键字左侧指针所指子树中“最右下”的元素
直接后继: 当前关键字右侧指针所指子树中“最左下”的元素
删除非终端节点----》转化为终端节点
- 删除82
删除终端节点:
兄弟节点够借
- 删除38:
- 删除90:
兄弟不够借:
- 删除49:
总结:
3.4 B+树
查找:
也可以从p指针开始,顺序查找
类比b-树的查找:
B+树 VS B树
MySQL是用的B+树
4.散列查找
数据元素的关键字和其存储地址直接相关
4.1哈希表的概念
哈希表的概念、
1.冲突的解决---》拉链法
散列查找:
效率分析:
4.2常用的哈希函数
1.除留取余法
2.直接定址法
3.数字分析法
4.平方取中法
总结:
4.3解决冲突的方法
1.拉链法(链地址法)
散列查找:
效率分析:
装填因子:
拉链法小优化:
2.开放定址法
开放定址法在现实中应用似乎不多;较多的用链地址法(拉链法);
开放定址法的三种方式是通过,发生冲突时,增量序列的不同来区分的。
i、线性探测法 ⭐
就是发生冲突时,往后一个找空位置(增量随着冲突次数依次+1)
哈希函数是对不大于表长的最大质数取模,
重定位是对 表长 取模
存放19, 14, 23,时无冲突;
存放1,发生冲突:
存放68, 20无冲突;
存放84发生冲突:
余数+1 再次发生冲突; 再次发生冲突:
查找操作:
- 查27:
- 查11:
- 查21:(失败)
删除操作:
- 删除1:
不标记的话,当查找的时候遇到空位置,直接就停止查找,返回找不到了
前面的都删除了,查找79:
效率很低(冲突次数多),但是考研喜欢考
查找效率分析:
一个一个元素地确定查找长度
ii.平方探测法 ⭐
是为了解决线性探测法查找时候,同义词、非同义词堆积现象:
根据第几次冲突,增量d是确定的
第一次冲突d1:1²;
第二次冲突d2:-1²;
第三次冲突d3:2²;
第四次冲突d4:-2²;
第五次冲突d5:3²;
。。。。。。
插入:
查找:
注意:查找的方式(次序)是根据增量序列来决定的;
难理解点:
由《数论》推出来的,不用管
即:如果散列表表长m满足:4j+1,则探测了m个位置之后,肯定会把散列表探测完;
如果不满足,就探测不完;
iii.伪随机序列法
di是个伪随机序列(事先给出的,确定的)
3.再散列法
即:多准备几个哈希函数,当发生冲突时候,用第二个,在发生冲突时,用第三个.......
按严蔚敏版的来,王道的不太对;
第八章:排序
7.排序
排序的基本概念;
插入类排序(直接插入排序、折半插入排序、希尔排序)、
交换类排序(冒泡排序、快速排序)、选择类排序(简单选择排序、堆排序)、
归并类排序(二路归并排序)、基数排序;
各种内部排序算法的稳定性和时间 复杂度与空间复杂度分析;
排序算法的应用。
1.排序的基本概念
1.1什么是排序
1.2排序算法的评价指标
- 时间复杂度
- 空间复杂度
- 算法的稳定性
1.3排序的分类
2.插入类排序
插入类排序(直接插入排序、折半插入排序、希尔排序)-----》都稳定
2.1插入排序
1.算法思想
算法思想:每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成。
2.步骤
3.算法实现
带哨兵:把temp变量用0号数组位置 取代
优点:不用每轮循环都判断 J>=0
4.效率分析
i.空间复杂度
原地工作,或者只需要一个temp变量 ,空间复杂度为O(1)
ii.时间复杂度
时间开销来源:对比关键字 + 移动元素
最好时间复杂度:
最坏时间复杂度:
2.2折半插入排序
1.算法思想
------插入排序的优化
对某一个元素进行插入排序的时候,前面的所有元素已经排序好了
----》因此可以用折半查找在前面的已经排好序的元素中找到该元素应放的位置
当low>high 时,停止折半查找,将要排序的元素插入到low所在的位置(先把大于该元素的元素右移,在将该元素插入)
2.算法实现
3.效率分析
-
空间复杂度和 插入排序一样;O(1)
-
比较关键字次数减少了,但是移动元素的次数没有改变,整体来看时间复杂度依然是O(n²)
-
算法是稳定的;(折半查找时候,mid = key时,依然继续查找,low = mid+1
4.延伸--对链表进行插入排序
移动元素可以不必一个一个移动了
但是查找还是必须一个一个找(没有随机存取的特性);
2.2希尔排序
-----基于插入排序的优化
追求局部有序,再逐渐逼近全局有序
代码不太考,考的话,记不住也可以自己根据这个思想,自己写代码
1.算法思想
先分小组,小组一开始很小,使小组间有序,小组越分越大,直到包含整个要排序的序列
2.步骤
第一趟:
增量d1 = L;(L为待排序序列长度)
第一趟:对每个子表进行直接插入排序
第二趟:
经过第一趟处理后:
缩小增量的值:d2 = d1 /2(扩大了小组的规模)
第二趟:对每个子表进行直接插入排序
第三趟:
经过第二趟处理后:
缩小增量的值:d3 = d2 /2(扩大了小组的规模) 此时只有一个小组,等于整个表长,表内的各个元素已经实现了基本有序
对整个表进行直接插入排序
总结:
第一趟增量选 元素个数/2 之后每次缩短一半; 别的增量策略也可以 : 首次增量,每次增量的变化,都可以自定义
3.算法实现
注意第三层for,实现小组内插入排序(第一遍小组内只有两个元素,用不到)
i 最初标识每个小组的第二个元素位置 (这样第三层for才能对小组内直接插入排序)
第一趟:
第二趟:
每个小组内插入排序交替进行(i每次+1,都会切换小组) -----》这里的代码是这样的,也可以一次处理完整个小组,再处理下个小组....
。。。。。。。。。。。。。。。
第三趟:
4.效率分析
-
空间复杂度:O(1);
-
时间复杂度:不确定,d=1时候,退化为插入排序O(n²);最坏也是O(n²); n在某个范围时,可达到O(n1.3)
-
稳定性:不稳定
-
不适用于链表(没有随机存取特性)
3.交换类排序
交换类排序(冒泡排序、快速排序)----》都不稳定
3.1冒泡排序
如果某趟冒泡没有发生一次交换,说明已经排序完成,可以提前结束
1.算法思想
每次排序好一个元素(最大或最小)
2.步骤
。。。。。。。。。。。。。。。
3.算法实现
4.效率分析
- 空间复杂度: O(1);
- 时间复杂度
- 最好:O(n);
- 最差:O(n²)
- 平均:O(n²)
- 稳定性:稳定
5.拓展---是否能用于链表?
可以
3.2快速排序 ⭐
快速排序的是我们学的排序算法里平均性能最优秀的
1.算法思想
找一个枢轴(基准)元素(通常用表的第一个元素),其左边元素小于该元素,其右边元素大于等于该元素;
2.步骤
high指针 和 low指针 依次移动(根据基准,找到需要移动的元素,移动后,换指针移动),(移动后其所指位置就空出来了)
。。。。。。。。。。。
如此用同样的操作处理左右子表,知道子表长度为1,或者0
。。。。。。。。。。。。。。。。。。。。
最终:
3.算法实现
递归
4.效率分析
- 空间复杂度:与递归工作栈的大小有关 O(递归层数);
- 最好:O(log2n)
- 最差:O(n)
- 时间复杂度: 取决于交换的次数 和 移动的次数 ,与循环次数无关(循环不满足的话,就不用交换了) O(n*递归层数)
- 最好:O(nlog2n)
- 最差:O(n²)
- 平均:O(n²)
- 稳定性:不稳定
时间空间复杂度:
稳定性:
总结:
对于一趟排序:
408: 一次划分 ≠ 一趟排序 一般的认为 一趟排序 = 一次划分
4.选择类排序
选择类排序(简单选择排序、堆排序)------》不稳定 (需要交换)
4.1简单选择排序
1.算法思想
2.步骤
3.算法实现
4.效率分析
稳定性:
4.2.堆排序
1.大根堆、小根堆
---》基于二叉树的 父子节点的关系
----对比二叉排序树BST(左《 根 《 右)
如何基于“堆”进行排序?
选择排序:每次调出最小的或者最大的;
假如能把待排序列整理成大根堆或者小根堆,每次从堆顶选出元素即可(最大,或最小)
2.建立大根堆
2.1思路
从后往前依次处理分支节点;
大根堆、小根堆都是 完全二叉树;
完全二叉树的 分支节点: (结合满二叉树,一个一个减去成为完全二叉树,2 i次方 - 1= 0次方 +2 + 2² + .....+ 2 (i-1)次方)
2.2手算步骤
从编号最大的分支节点从后往前进行调整
不满足,将最大的孩子和其对调
2.3代码实现
小元素下坠,大元素上升
headAdjust里的else里的 k= i是 为了向下调整,防止置换到下层的元素,置换后不满足大根堆特性,继续调整
小元素下坠
3.基于大根堆进行选择排序
注意: 大根堆建立后,还是存放在数组里的
将堆顶元素和待排序元素最后一个交换,(交换后位置就确定了,不用再动了)
接着,调整堆,使其再次满足大根堆特性(堆顶是最大元素)
如此循环即可
第一趟:
调整大根堆
第二趟:
调整大根堆
第三趟:
调整大根堆
第四趟:
调整大根堆:
。。。。。。。。。。。。。。。。。。。。。
如此重复处理
第七趟:
第七趟排完后,最后一个元素不需要再调整了
总结:
n个元素的序列,经过n-1趟处理后,即可得到有序的序列
4.算法实现
5.效率分析
稳定性:
建堆是通过交换实现的,因此是不稳定的
6.扩展练习
4.2.1堆的插入和删除
1.插入
新插入节点放到数组最后一个地方;然后调整堆,使其符合堆的要求
插入13:
第一次上升,对比其 和 其父节点(因为未插入前父节点一定小于其孩子,所以插入的元素不用和其兄弟比较) 一次对比;
故每上升一层,对比一次;
插入46:
2.删除
我想的是:取其较小的孩子代替其位置,继续对该孩子如此操作,直到没有孩子
教材给出的是:用最后一个位置的元素代替其(同时length--),然后让其下坠
删除13:
第一次下坠:对比两个孩子找出较小的孩子,对比其 和 较小的孩子 2次对比;(因为提上去的节点不知道多大,需要和每个孩子比较)
第二次下注:对比两个孩子找出较小的孩子,对比其 和较小的孩子 2次对比。
故下坠一层,比较两次;(这么看来,我的方法效率提升一倍啊)
删除65:
5.归并类排序
归并类排序(二路归并排序)、
1.二路归并
内部排序用 二路归并;
归并排序用于外部排序时:一般用多路归并.
(内部排序:将待排序序列全部调入内存; 外部排序:待排序序列大小太大,只能部分调入内存,部分还在磁盘)
1.算法思想
2.步骤
核心操作:把数组内的两个有序序列归并为一个
初始时,把每个元素看成一个有序序列,相连两个元素进行归并排序;
3.算法实现(代码)
递归
i、核心操作
用low, mid ,high三个指针区分开两个有序序列的范围;
ii、完整代码
递归:
4.效率分析
时间复杂度:
空间复杂度:
稳定性:稳定的,(对两个序列归并,遇到相等时,先将前面的归并即可)
最好,最坏,平均时间复杂度都一样:O(nlog2n)
空间复杂度来自于 辅助数组,把原数组复制了一遍(O(n))
递归工作栈也需要空间,但是数量级为 ( O(递归层数),即O(log2n) ) ,去大头为:O(n)
递归的时候,内层的函数也会复制短的序列,加一起长度为n;
2.多路归并
3.基数排序
radix sort
一般不考算法实现;不过也很简单的;
1.算法思想
示例是得到递减的有序序列
将关键字拆分成三部分:个位, 十位, 百位;
辅助队列
分配
收集
思想:
递增 递减 的区别是收集的顺序不同
i、第一趟:以个位进行分配;
此时个位数字已经有序,后序的处理,也会使得更高位数字相同的不同元素 个位数字相对有序
ii、第二趟以十位进行分配
基于已经个位相对有序的序列,再使其十位有序
当两个关键字的十位数字相同时,个位数更大的会先入队
此时个位 ,十位 数字已经相对有序,后序的处理,也会使得更高位数字相同的不同元素 个位 、十位数字相对有序
iii.第三趟,按百位进行分配
2.效率分析
基本基于链式存储来实现
空间复杂度:
空间消耗来源于辅助队列的空间占用;每个权位 的取值范围为基数r (例子中的个位十位。。都能取0——9,十个数字,就需要十个辅助队列)
时间复杂度:
注意:收集的时间复杂度为O(r);每个队列的处理时长为O(1),是因为直接把队列的链尾连到下一个链的链头即可;
稳定性:
稳定的
3.基数排序的应用.
这里的每一轮的 基数r都不同,年为6,月为12,日为31
对于这种情景的应用,基数排序的时间复杂度比快速排序还高效
反例、擅长:
擅长分组d小,每组关键字取值范围小,元素个数大
各种内部排序算法的稳定性和时间 复杂度与空间复杂度分析;
排序算法的应用。
本文来自博客园,作者:岂能事事顺心如意,转载请注明原文链接:https://www.cnblogs.com/LJK66666/p/16719963.html