一. Start
1.基本概念
数据对象:是具有相同性质的数据元素的集合,是数据的一个子集。
数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。
数据元素:是数据的基本单位,通常作为一个整体进行考虑和处理。
数据项:一个数据元素可由若干数据项组成,数据项是构成数据元素的不可分割的最小单位。
同样的数据元素,可组成不同的数据结构。
不同的数据元素,可组成相同的数据结构。
2.数据结构三要素
- 逻辑结构 (常见:集合,线性结构,树形结构,图结构)
- 数据的运算(增删查改)
- 物理结构(存储结构)
- 顺序存储
- 链式存储
- 索引存储
- 散列存储:根据元素的关键字直接计算出该元素的存储地址,又称哈希存储。
数据的存储结构会影响存储空间分配的方便程度。
数据的存储结构会影响数据运算的速度。
数据类型是一个值的集合 和 定义在此集合上的一组操作的总称。
3.算法的基本概念
算法:求解问题的步骤
算法特性
- 有穷性
- 确定性:对于每种情况下所应执行的操作,在算法中应该有确切的规定
- 可行性
- 输入
- 输出
好算法
- 正确性
- 可读性
- 健壮性
- 高效率与低存储量需求(时间复杂度 空间复杂度)
4.1时间复杂度 T(n)
多项相加,只保留最高阶的项,且系数变为1
多项相乘,都保留
O(1) < O( log 2 n \log_2 n log2n)<O(n)<O(n log 2 n \log_2 n log2n)<O( n 2 n^2 n2)<O( n 3 n^3 n3)<O( 2 n 2^n 2n)<O(n!)<O( n n n^n nn)
速记:常对幂指阶
一层循环:
- 列出循环趟数t及每轮循环i的变化值
- 找到t与i的关系
- 确定循环停止条件
- 解方程
两层循环:
- 列出外层循环中i的变化值
- 列出内层语句的执行次数
- 求和,写结果
多层循环
- 方法一:抽象为计算三维体积
- 方法二:列式求和
4.2 空间复杂度S(n)
算法原地工作:算法所需内存空间为常量
二. 线性表
1.1线性表定义
具有相同数据类型的n个数据元素的有限序列。
L=( a 1 a_1 a1 a 2 a_2 a2 a a a a i a_i ai a n a_n an)
a i a_i ai是线性表中的‘第i个’元素在线性表中的位序(位序从1开始, 下标从0开始)
1.2线性表基操
创 销 增 删 改 查
2.1 顺序表(顺序存储)
逻辑上相邻的元素 其物理的存储位置是连续的
顺序表特点
- 随机访问
- 存储密度高,每个节点只存储数据元素
- 拓展容量不方便
- 插入,删除不方便
#include <stdio.h>
#define maxSize 10
//顺序表静态分配
typedef struct{
int data[maxSize];
int length;
}SqList;
void InitSqList(SqList &L){
L.length=0;
}
int main(){
SqList L;
InitSqList(L);
printf("%d\n",L.length);
return 0;
}
#include <stdlib.h>
#include <stdio.h>
#define InitSize 10
//顺序表动态分配
typedef struct{
int *data;
int maxSize;
int length;
}SqList;
void InitSqList(SqList &L){
L.data = (int *)malloc(sizeof(int)*InitSize);
L.maxSize = InitSize;
printf("init ---- %d\n",L.maxSize);
L.length=0;
}
void IncreaseValue(SqList &L, int size){
int *p = L.data;
L.data = (int *)malloc((L.maxSize+size)*sizeof(int));
for(int i = 0; i< L.length; i++){
L.data[i] = p[i];
}
L.maxSize = L.maxSize+size;
printf("increase ---- %d\n",L.maxSize);
free(p);
}
int main(){
SqList L;
InitSqList(L);
IncreaseValue(L,5);
printf("main --- %d\n",L.maxSize);
return 0;
}
顺序表-插入 c语法
#include <stdio.h>
#define maxSize 10
typedef struct {
int data[maxSize];
int length;
} SqList;
void InitSqList(SqList *L) {
L->length = 0;
}
void insert(SqList *L, int index, int value) {
if (index < 0 || index > L->length || L->length == maxSize) {
return; // 插入位置无效或顺序表已满
}
for (int i = L->length; i > index; i--) {
L->data[i] = L->data[i - 1];
}
L->data[index] = value;
L->length++;
}
void print(SqList *L) {
for (int i = 0; i < L->length; i++) {
printf("%d\n", L->data[i]);
}
}
int main() {
SqList L;
InitSqList(&L);
insert(&L, 0, 66);
insert(&L, 1, 66);
insert(&L, 1, 96);
print(&L);
return 0;
}
最好时间复杂度 O(1)
最坏时间复杂度O(n)
顺序表-删除
与插入相反。 可以把删除的元素返回。
顺序表-查找
- 按位查找
- 按值查找
最好时间复杂度 O(1)
最坏时间复杂度O(n)
2.2 链表(链式存储)
2.2.1 单链表
// my_includes.h
#ifndef MY_INCLUDES_H
#define MY_INCLUDES_H
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
// 可以继续添加其他需要的头文件
#endif // MY_INCLUDES_H
//单链表 定义 初始化 插入
#include "my_includes.h"
typedef int ElemType;
typedef struct LNode{
ElemType data;
struct LNode * next;
} LNode, *LinkList; // LinkList 是 指向LNode的指针的别名。
//初始化 带头节点的链表
bool InitLinkList(LinkList *L){
LNode *head = (LNode *)malloc(sizeof(LNode));
if(head == NULL){
return false;
}
head->next = NULL;
*L = head;
return true;
}
//追加
void add(LinkList *L, ElemType value){
LNode * current = *L; //头指针
LNode * newNode = (LNode *)malloc(sizeof(LNode));
newNode->data = value;
newNode->next = NULL;
while(current->next != NULL){
current = current->next;
}
current->next = newNode;
}
//后插操作,给定一个节点
bool insertNextNode(LNode * node, ElemType value){
if(node == NULL){
return false;
}
LNode * newNode = (LNode *)malloc(sizeof(LNode));
if(newNode == NULL){
return false;
}
newNode->data= value;
newNode->next = node->next;
node->next = newNode;
return true;
}
//前插操作
bool insertBeforeNode(LNode * node, ElemType value){
if(node == NULL){return false;}
LNode * newNode = (LNode *)malloc(sizeof(LNode));
if(newNode == NULL){return false;}
newNode->next = node->next;
node->next = newNode;
newNode->data = node->data;
node->data = value;
return true;
}
//插入 有头节点
//按照 位序
bool InsertByOrder(LinkList *L, int order, ElemType value){
if(order<1){
return false;
}
// LNode * newNode = (LNode *)malloc(sizeof(LNode));
LNode * current = *L; //扫描
int j = 0; //记录当前指针所在的下标
while(current != NULL && j<order-1){
current = current->next;
j= j+1;
}
if(current == NULL){
return false;
}
insertNextNode(current, value);
// insertBeforeNode(current,value);
// newNode->data = value;
// newNode->next = NULL;
// newNode->next = current->next;
// current->next = newNode;
return true;
}
//删除 按照位序
ElemType deleteByOrder(LinkList *L, int order){
LNode * current = *L;
int j = 0;
while(current != NULL && j < order - 1){
current = current->next;
j++;
}
ElemType rValue = current->next->data;
LNode * temp = current->next;
current->next = current->next->next;
free(temp);
return rValue;
}
//删除 指定节点
bool deleteByNode(LNode * node){
if(node == NULL){
return false;
}
LNode * temp = node->next;
if(temp == NULL){
return false;
}
node->data = temp->data;
node->next = temp->next;
free(temp);
return true;
}
//按位查找
LNode * GetElemByOrder(LinkList L, int order){
if(order < 1){
return NULL;
}
int frequency = 0;
while(L != NULL && frequency < order){
L = L->next;
frequency++;
}
return L;
}
//按值查找
LNode * GetElemByValue(LinkList L , ElemType value){
if(L == NULL){
return NULL;
}
while(L != NULL){
if(L->data == value){
return L;
}else{
L = L->next;
}
}
return NULL;
}
//求表长度
int Length(LinkList L){
int len=0;
L = L->next;
while(L!=NULL){
L = L->next;
len++;
}
return len;
}
//打印
void printLinkList(LinkList *L){
LNode * current = *L;
while(current->next != NULL){
printf("%d\n", current->next->data);
current = current->next;
}
}
int main(){
LinkList L; //声明一个单链表 L
InitLinkList(&L); //初始化
add(&L, 1);
add(&L, 2);
add(&L, 3); //添加三个元素
InsertByOrder(&L,3,666); //插入元素
ElemType value = deleteByOrder(&L,3);//删除
printLinkList(&L);//打印
printf("---------%d\n", value);
LNode* getElem = GetElemByOrder(L,2);
if(getElem != NULL){
printf("getElem %d\n",getElem->data);
}else{
printf("error ");
}
LNode * getElemByValue = GetElemByValue(L,1);
if(getElemByValue != NULL){
printf("getElemByValue %d\n",getElemByValue->data);
}else{
printf("error ");
}
int len = Length(L);
printf("len :%d",len);
return 1;
}
[Running] cd "e:\DS\" && gcc LinkList.c -o LinkList && "e:\DS\"LinkList
1
2
3
---------666
getElem 2
getElemByValue 1
len :3
[Done] exited with code=1 in 0.221 seconds
单链表的建立(头插法,尾插法)
//尾插法
#include "my_includes.h"
typedef int ElemType;
typedef struct LNode{
ElemType data;
struct LNode * next;
} LNode, *LinkList;
bool InitLinkList(LinkList *L){
LNode * head = (LNode *)malloc(sizeof(LNode));
if(head == NULL){
return false;
}
head->next = NULL;
*L = head;
return true;
}
LinkList LinkList_TailInsert(LinkList *L){
if(*L == NULL)
{
return NULL;
}
int x;
printf("please:");
scanf("%d",&x);
LNode * current = *L;
while(x !=9999){
LNode * newNode = (LNode *)malloc(sizeof(LNode));
newNode->data = x;
newNode->next = NULL;
current->next = newNode;
current = current->next;
printf("please");
scanf("%d",&x);
}
return *L;
}
void printLinkList(LinkList *L){
LNode * current = *L;
while(current->next != NULL){
printf("%d\n", current->next->data);
current = current->next;
}}
int main(){
LinkList L;
InitLinkList(&L);
LinkList L1 = LinkList_TailInsert(&L);
printLinkList(&L1);
return 1;
}
链表的逆置可以用头插法
LinkList LinkList_HeadInsert(LinkList *L){
if(*L == NULL){
return false;
}
LNode * current = *L;
int x;
printf("please input:");
scanf("%d",&x);
while(x != 9999){
LNode * newNode = (LNode *)malloc(sizeof(LNode));
newNode->next = NULL;
if(newNode == NULL){
return NULL;
}
newNode->data = x;
if(current->next == NULL){
current->next = newNode;
}else{
newNode->next = current->next;
current->next = newNode;
}
printf("please input:");
scanf("%d",&x);
}
return *L;
}
2.2.2 双链表
单链表无法逆向检索
双链表可以逆向检索,但是存储密度稍低
#include "my_includes.h"
typedef int ElemType;
typedef struct LNode{
ElemType data;
struct LNode * next; //指向下一个节点
struct LNode * prior; //指向前一个节点
} LNode, *DLinkList;
bool InitDLinkList(DLinkList *L){
//带头节点的双向链表
LNode * newNode = (LNode *)malloc(sizeof(LNode));
if(newNode == NULL){
return false;
}
newNode->prior = NULL;
newNode->next = NULL;
*L = newNode;
return true;
}
//插入
bool add(DLinkList *L, ElemType value){
if(*L == NULL){
return false;
}
LNode * newNode = (LNode *)malloc(sizeof(LNode));
newNode->data = value;
newNode->next = NULL;
LNode * current = *L;
if(current->next == NULL){
newNode->prior = current;
current->next = newNode;
}else{
while(current->next != NULL){
current = current->next;
}
current->next = newNode;
newNode->prior = current;
}
return true;
}
//双链表的删除
ElemType deleteByorder(DLinkList* L, int order){
// if(*L == NULL){
// return NULL;
// }
if(order < 1){
return NULL;
}
LNode * current = *L;
int times = 0;
while(current != NULL && times < order - 1){
current = current->next;
times++;
}
ElemType value = current->data;
current->next->prior = current->prior;
current->prior->next = current->next;
free(current);
return value;
}
//打印
void printLinkList(DLinkList *L){
LNode * current = *L;
while(current->next != NULL){
printf("%d\n", current->next->data);
current = current->next;
}
}
int main(){
DLinkList L;
InitDLinkList(&L);
add(&L,1);
add(&L,2);
add(&L,3);
printLinkList(&L);
ElemType value = deleteByorder(&L,2);
printf("----------------------------\n");
printLinkList(&L);
printf("delete value:%d\n",value);
return 1;
}
2.2.3 循环链表
循环单链表 :头部和尾部操作频率高的场景
循环双链表
2.2.4 静态链表
分配一整片连续的内存空间,各个结点集中安置
用数组方式实现链表
数据元素量固定不变的场景(os的文件分配表FAT)
#include "my_includes.h"
typedef int ElemType;
struct Node{
ElemType data;
int next;
};
2.3.4 顺序表和链表比较
链表:表长难以预估、经常增删元素
顺序表:表长可预估、经常查询元素
三. 栈
3.1 栈的定义
栈(stack):是只允许在一端进行插入或删除操作的线性表。
特点:后进先出 Last In First Out (LIFO)
栈顶:允许插入和删除的一端。
3.2 栈的基操
- 初始化
- 销毁
- 出栈 pop
- 入栈 push
- 读栈顶元素 gettop
- 是否为空
tips: n个不同元素进栈, 出栈元素不同排列的个数为 1 / ( n + 1 ) 1/(n+1) 1/(n+1) C 2 n n C^n_2n C2nn
3.3 顺序栈
#include "my_includes.h"
#define maxSize 10
typedef int ElemType;
typedef struct Sqstack{
ElemType data[maxSize]; //静态数组存放元素
int top; //栈顶指针
} Sqstack;
bool InitSqStack(Sqstack* S){
S->top = -1; //有两种实现方式,一种top指向当前元素,另一种top指向下一个元素
return true;
}
//进栈
bool push(Sqstack* S, ElemType value){
if(S->top == maxSize-1){
return false; //栈满
}
S->data[++S->top] = value;
}
ElemType pop(Sqstack* S){
ElemType value = S->data[S->top--];
return value;
}
//打印
void printStack(Sqstack* s){
int times = s->top;
while(times != -1){
printf("%d--",s->data[times]);
times--;
}
}
int main()
{
Sqstack S;
bool value = InitSqStack(&S);
push(&S,1);
push(&S,2);
push(&S,3);
printStack(&S);
ElemType valuepop = pop(&S);
printf("pop :%d", valuepop);
return 1;
}
补充:共享栈,有两个栈顶指针
3.4 链栈
和单链表差不多,规定只能在一端操作。 比如规定头节点为栈顶。
用头插法实现 入栈 出栈操作。
#include "my_includes.h"
typedef int Elemtype;
typedef struct ListStack{
Elemtype data;
struct ListStack * next;
} Node, *Stack;
//初始化 不带头节点
bool InitStack(Stack *S){
*S == NULL;
return true;
}
bool push(Stack *S, Elemtype value){
Node * newNode = (Node *)malloc(sizeof(Node));
if(newNode == NULL){
return false;
}
newNode->data = value;
newNode->next = NULL;
if(*S == NULL){
*S = newNode;
return true;
}
newNode->next = *S;
*S = newNode;
return true;
}
void printStack(Stack * S){
Node * current = *S;
while(current!=NULL){
printf("%d\n",current->data);
current = current->next;
}
}
int main(){
Stack S;
InitStack(&S);
push(&S,1);
push(&S,2);
push(&S,3);
printStack(&S);
return 1;
}
四.队列
4.1 概念
队列(Queue):是只允许在一端进行插入,在另一端删除的线性表
特点:先进先出(FIFO) First In First Out
相关术语:
- 队头:允许删除的一端
- 队尾:允许插入的一端
4.2 队列的基操
- 初始化
- 销毁
- 入队
- 出队
- 读队头元素
4.3 队列的顺序实现
#include "my_includes.h"
#define maxSize 5
typedef int Elemtype;
typedef struct SqQueue{
Elemtype data[maxSize];
int front, rear; //队头指针和队尾指针
} SqQueue;
bool InitQUeue(SqQueue *Q){
Q->front =0;
Q->rear = 0;
return true;
}
//入队
bool EnQueue(SqQueue *Q, Elemtype value){
if((Q->rear+1)%maxSize == Q->front){
printf("queue is full!\n");
return false;
}
Q->data[Q->rear] = value;
Q->rear = (Q->rear+1)%maxSize;
return true;
}
//出队 队头元素
Elemtype DeQUeue(SqQueue * Q){
if(Q->rear == Q->front){
return 9999; //这里不合理,随便写的
}
Elemtype Fvalue = Q->data[Q->front];
Q->front = (Q->front+1)%maxSize;
return Fvalue;
}
//打印 测试使用
void printQueue(SqQueue S){
while(S.front!= S.rear){
printf("%d-", S.data[S.front++]);
}
printf("\n");
}
int main(){
SqQueue S;
InitQUeue(&S);
EnQueue(&S, 1);
EnQueue(&S, 2);
EnQueue(&S, 3);
printQueue(S);
printQueue(S);
EnQueue(&S, 4);
EnQueue(&S, 5);
printQueue(S);
Elemtype fvalue = DeQUeue(&S);
printf("fvalue = %d\n",fvalue);
printQueue(S);
return 1;
}
4.4 队列的链表实现
#include "my_includes.h"
typedef int ElemType;
typedef struct Node{
ElemType data;
struct Node *next;
} Node;
typedef struct LinkQueue{
Node * front;
Node * rear;
} LinkQueue;
//初始化 带头结点
bool InitQUeue(LinkQueue * Q) {
Node * newNode = (Node *)malloc(sizeof(Node));
if(newNode == NULL){
return false;
}
Q->front = newNode;
Q->rear = newNode;
Q->front->next = NULL;
return true;
}
//入队
bool EnQueue(LinkQueue *Q, ElemType value){
Node * nodeNew = (Node * )malloc(sizeof(Node *));
if(nodeNew == NULL){
return false;
}
nodeNew->data = value;
nodeNew->next = NULL;
Q->rear->next = nodeNew;
Q->rear = nodeNew;
return false;
}
//出队
ElemType DeQueue(LinkQueue *Q){
if(Q->front == Q->rear){
return 9999999;
}else{
ElemType Fvalue = Q->front->next->data;
Node *p = Q->front->next;
Q->front->next = Q->front->next->next;
free(p);
return Fvalue;
}
}
//测试打印
void prinfqueue(LinkQueue Q){
if(Q.front == Q.rear){
printf("queue is null\n");
}else{
Q.front=Q.front->next;
while(Q.front != NULL){
printf("%d\n",Q.front->data);
Q.front = Q.front->next;
}
}
}
int main(){
LinkQueue Q;
InitQUeue(&Q);
EnQueue(&Q,1);
EnQueue(&Q,2);
EnQueue(&Q,3);
prinfqueue(Q);
ElemType fvalue = DeQueue(&Q);
printf("fvalue= %d\n",fvalue);
prinfqueue(Q);
return 1;
}
4.5 双端队列
双端队列:只允许从两端插入、两端删除的线性表
输入受限的双端队列:只允许从一端插入、两端删除的线性表
输出受限的双端队列:只允许从两端插入、一端删除的线性表
栈中合法的序列,双端队列中一定也合法
五.栈和队列的应用
5.1 栈的应用
5.1.1 括号匹配问题
遇到 左括号 就入栈
遇到 右括号 就出栈一个 左括号
#include "my_includes.h"
typedef char ElemType;
typedef struct LinkStack{
ElemType data;
struct LinkStack * next;
} Node, * LinkStack;
bool InitStack(LinkStack * S){
//不带头结点
*S = NULL;
return true;
}
//入栈
bool push(LinkStack * S, ElemType value){
Node * newNode = (Node *)malloc(sizeof(Node));
if(newNode == NULL){
return false;
}
newNode->data = value;
newNode->next = NULL;
if(*S == NULL){
*S = newNode;
}else{
newNode->next = *S;
*S = newNode;
}
return true;
}
//出栈
ElemType pop(LinkStack * S){
if(*S == NULL){
return 'n';
}
ElemType fvalue = (*S)->data;
Node * temp = *S;
*S = (*S)->next;
free(temp);
return fvalue;
}
//判断栈空
bool isEmpty(LinkStack *S){
if(*S == NULL){
return true;
}
return false;
}
//括号匹配
bool bracketsMath(char str[],int length){
LinkStack S;
InitStack(&S);
for(int i = 0; i< length; i++){
if(str[i] == '(' || str[i] == '{' || str[i] == '['){
push(&S,str[i]);
}else{
if(isEmpty(&S) == true){ // 检测到右括号,但栈空
return false;
}
ElemType popvalue = pop(&S);
if(str[i] == ')' && popvalue !='(' ){
return false;
}
if(str[i] == ']' && popvalue !='[' ){
return false;
}
if(str[i] == '}' && popvalue !='{' ){
return false;
}
}
}
return isEmpty(&S); //最后应该栈空
}
//打印测试
void printfStack(LinkStack S){
while(S != NULL){
printf("%c\n",S->data);
S = S->next;
}
}
int main(){
// LinkStack S;
// InitStack(&S);
// push(&S,'{');
// push(&S,'(');
// push(&S,'[');
// printfStack(S);
// ElemType topvalue = pop(&S);
// printf("topvalue = %c\n", topvalue);
// printfStack(S);
char str[3] = {'(',')','['};
bool isMath = bracketsMath(str,3);
printf("%d",isMath);
return 1;
}
5.1.2 表达式求值问题
前缀表达式(波兰表达式 Polish notationd)
中缀表达式
后缀表达式(逆波兰表达式)
用栈实现 后缀表达式的计算
- 从左往右扫描下一个元素,直到处理完所有元素
- 若扫描到 数 则压入栈,并返回上一步,否则执行下一步
- 若扫描到运算符,则弹出两个栈顶元素,执行相应运算, 运算结果压入栈中,回到第一步
//中缀表达式转后缀表达式
p29
//中缀表达式的计算
5.1.3 递归问题
函数调用的特点:LIFO
5.2 队列的应用 --后续章节
5.2.1 树的层次遍历
5.2.1 图的广度优先遍历
六.矩阵的压缩存储
6.1 对称矩阵的压缩存储
- 只存储主对角线+下三角区
- 按行优先原则将各元素存入一维数组中( a i , j a_i,_j ai,j在一维数组中是第 i ( i − 1 ) 2 \frac{i(i-1)}{2} 2i(i−1)+j )
6.2 三角矩阵的压缩存储
- 按行优先原则将 主对角线和下三角区的元素存入一维数组中。 并在最后一个位置存储常量C
6.3 三对角矩阵的压缩存储
- 当|i-j|>1时, 有 a i , j a_i,_j ai,j=0
6.4 稀疏矩阵的压缩存储
- 非0元素远远少于矩阵元素的个数
- 顺序存储策略: 三元组<行,列,值>
- 链式存储策略: 十字链表法
七. 串
7.1 串的定义
串,即字符串(String):是由0个或多个字符组成的有限序列。
串是一种特殊的线性表
7.2 串的基操
- 赋值
- 复制
- 判空
- 串长
- 清空
- 销毁
- 联接
- 求子串
- 定位
- 比较大小
7.3 串的存储结构
7.3.1 顺序存储
#define MaxSize 255
typedef struct String{
char data[MaxSize];
int length;
} String;
7.3.2 链式存储
7.3.3 基于顺序存储实现基本操作
7.4 朴素模式匹配算法
- 主串长度为n,模式串长度为m
- 朴素模式匹配算法:将主串中所有长度为m的子串依次与模式串对比,直到找到一个完全匹配的子串,或所有的子串都不匹配为止。
- 最多对比n-m+1个子串
- 最坏时间复杂度 O(nm)
int Index(String MString, String SString){
//MString 主串 SString子串
//return :子串第一次出现的位置
int mi = 0; //记录主串当前位置
int si = 0; //记录子串当前位置
while(mi < MString.length && si<SString.length){
if(MString.data[mi] == SString.data[si]){
mi++;
si++;
}else{
mi=mi -si +3; //采用了下标从0开始,所以加三
si = 0;
}
}
return mi-SString.length+1; //下标从0开始,所以加1
}
7.5 KMP算法(后续在深入)
- 朴素模式匹配算法的缺点:当某些子串与模式串能部分匹配时,主串的扫描指针经常回溯
,导致时间开销增加- 最坏时间复杂度O(m+n)
步骤
- 根据模式串,求出next数组(重点)
- 利用next数组进行匹配
求next 数组(手动)
KMP算法优化
- 在next数组中, 当next[i]的值和映射的值相同时,可以跳过。
八. 树
8.1 树的基本概念
- 树是n(n>=0)个结点的有限集合,n=0时,称为空树。
- 任意一颗非空树应该满足:
有且仅有一个根节点。
当n>1时,其余结点可分为m(m>0)个互不相交的有限集合,其中每个集合本身又是一棵树,并且称为根结点的子树。
相关术语
- 路径:只能从上往下,两个结点之间
- 路径长度:经过几条边
- 结点的层次:从上往下数
- 结点的高度:从下往上数
- 树的高度:总共多少层
- 结点的度:有几个分支 (叶子结点度为0)
- 树的度:各结点的度的最大值
- m叉树:每个结点最多只能有m个孩子的树
- 有序树:逻辑上看,树中结点的各子树从左至右是有次序的,不能互换。
- 无序树
- 森林:是m颗互不相交的树的集合
8.2 树的性质
- 结点数=总度数+1
度为m的树 | m叉树 |
---|---|
任意结点的度<=m | 任意结点的度<=m |
至少有一个结点度=m | 允许所有结点的度都为m |
一定是非空树 | 可以是空树 |
- 度为m的树,第i层至多有 m i − 1 m^{i-1} mi−1个结点
- 高度为h的m叉树至多有 m h − 1 m − 1 \frac{m^h-1}{m-1} m−1mh−1个结点
- 高度为h的m叉树至少有h+m+1个结点
- 具有n个结点的m叉树的最小高度为
8.3 二叉树
- 是有序树
8.3.1 二叉树概念
- 特殊二叉树
- 满二叉树
1.只有最后一层有叶子结点
2.不存在度为1的结点
3.按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1 - 完全二叉树
1.只有最后两层可能有叶子结点
2.最多只有一个度为1的结点
3.按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1
4.i≤ ⌊ n 2 ⌋ \lfloor \frac{n}{2}\rfloor ⌊2n⌋为分支结点 - 二叉排序树
1.左子树上所有结点的关键字均小于根节点的关键字
2.右子树上所有结点的关键字均大于根节点的关键字 - 平衡二叉树
1.树上任意结点的左子树和右子树的深度之差不超过1
2.能有效提高搜索效率
- 满二叉树
二叉树性质
- 设非空二叉树中度为0、1和2的结点个数分别为 n 0 n_0 n0, n 1 n_1 n1 和 n 2 n_2 n2,则 n 0 n_0 n0= n 2 n_2 n2+1(叶子节点比二分之结点多一个)
完全二叉树
8.3.2 二叉树的存储结构
8.3.2.1 顺序存储
二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来
顺序存储结构, 只适合存储完全二叉树
8.3.2.2 链式存储
8.3.3 二叉树遍历
- 先序遍历:根左右
- 中序遍历:左根右
- 后序遍历:左右根
typdef struct BiTNode{
Elemtype data;
struct BiTNode *lchild, * rchild;
} BiTNode, * BiTree;
//先序遍历
void PreOrder(BiTree T){
if(T != NULL){
vivst(T); //打印
PreOrder(T.lchild);
PreOrder(T.rchild);
}
}
#include <stdio.h>
#include <stdlib.h>
typedef struct TreeNode {
int data;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
// 创建一个新的树节点
TreeNode* createNode(int data) {
TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));
if (newNode == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(EXIT_FAILURE);
}
newNode->data = data;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}
// 插入新值到二叉搜索树中
TreeNode* insertIntoBST(TreeNode* root, int data) {
if (root == NULL) {
root = createNode(data);
return root;
}
if (data < root->data) {
root->left = insertIntoBST(root->left, data);
} else if (data > root->data) {
root->right = insertIntoBST(root->right, data);
}
// 等于的情况通常不插入,但您可以根据需要修改
return root;
}
// 一个简单的中序遍历函数来打印树(二叉搜索树的中序遍历是排序的)
void inorderTraversal(TreeNode* root) {
if (root != NULL) {
inorderTraversal(root->left);
printf("%d ", root->data);
inorderTraversal(root->right);
}
}
int main() {
TreeNode* root = NULL;
int data;
printf("Enter integers (use -1 to stop): ");
while (scanf("%d", &data) && data != -1) {
root = insertIntoBST(root, data);
}
printf("Inorder traversal of the created BST:\n");
inorderTraversal(root);
printf("\n");
// 注意:这里省略了释放树的代码以保持简洁性
return 0;
}
层序遍历
初始化一个辅助队列
根节点入队
若队列非空,则队头结点出队,访问该结点,并将其左右孩子入队
重复上一步骤
由遍历序列构造二叉树
若只给出一颗二叉树的 前/中/后/层 序遍历序列中的一种,不能唯一确定一颗二叉树
中序与其他任意一种序列 即可构造二叉树
- key:找到树的根结点,并根据中序序列划分左右子树,再找到左右子树根节点
8.4 线索二叉树
8.4.1 概念
指向前驱、后继的指针称为“线索”
利用二叉树本身的n+1个空链域
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag, rtag;
//当tag == 0,表示指针指向孩子结点
//当tag == 1,表示指针是线索
}ThreadNode , * ThreadTree;
8.4.2 代码
todo p50 p51 后续
8.5 树的存储结构
8.5.1双亲表示法(顺序存储)
8.5.2孩子表示法(顺序+链式存储)
8.5.3孩子兄弟表示法(链式存储)
用二叉链表存储树----左孩子右兄弟
森林和二叉树的转换
二叉树中 : 左孩子是孩子, 右孩子转换为兄弟结点。
森林中各个树的根结点之间视为兄弟关系
8.6 树和森林的遍历
8.6.1 树的遍历
树的先根遍历
先访问根结点,在对每棵子树进行先根遍历。
树的先根遍历与这棵树相应二叉树的先序序列相同。
树的后根遍历
树的后根遍历与这棵树相应二叉树的中序序列相同。
树的层次遍历
用队列实现
8.6.2 森林的遍历
森林的先序遍历
森林的中序遍历
相当于对各个子树进行后根遍历
8.7 哈夫曼树(最优二叉树)
结点的权:有某种现实含义的数值(不同结点的重要程度不同)
结点的带权路径长度:从树的根到该结点的路径长度与该结点上权值的乘积
树的带权路径长度:树中所有叶子结点的带权路径长度之和(WPL)
哈夫曼树:在含有n个带权叶子结点的二叉树中,其中带权路径长度最小的二叉树称为哈夫曼树
哈夫曼树的构造
- F中有n个结点
- 在n个结点中取出两个权值最小的结点, 其权值之和为其根结点的权值,并将根结点权值放入F中
- 重复
哈夫曼编码
若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码
8.8并查集(适合使用双亲表示法)
如何查到一个元素属于哪一个集合:从指定元素出发,找到根结点
根结点相同,两个元素属于同一个集合
两个集合“并”,让一棵树称为另一棵树的子树
#define MAX_TREE_SIZE 100 //树中最多有100个结点树
#define SIZE 13
int UFSets[SIZE];
typedef int ElemType;
typedef struct{
ElemType data;
int parent;
}PTNode;
typedef struct{
PTNode nodes[MAX_TREE_SIZE];
int n;
}PTree;
void Initial(int S[]){
for(int i=0; i< SIZE;i++)
S[i] = -1;
}
int find(int S[], int x){
while(S[x] >=0)
x=S[x];
return x;
}
//void union(int S[], int Root1, int Root2){
// if(Root1 == Root2) return;
// S[Root2]=Root1;
//}
//优化 //小树合到大树,树的高度增加会减缓
void union(int S[], int Root1, int Root2){
if(Root1 == Root2) return;
if(S[Root2] >S[Root1]){
S[Root1] += S[Root2];
S[Root2] =S[Root1];
}else{
S[Root2] += S[Root1];
S[Root1] =S[Root2];
}
S[Root2]=Root1;
}
并查集Find优化-压缩路径
九.图
1.图的定义
- 图G由顶点集V和边集E组成,记为G=(V,E)
- 图不可以是空,即V一定是非空集
无向图 有向图
简单图 多重图
后续
2.图的存储
2.1 邻接矩阵法(适合存储稠密图)
空间复杂度高
#define maxvertexnum 100 //顶点数目的最大值
typedef struct{
char Vex[maxvertexnum ]; //顶点
int Edge[maxvertexnum ][maxvertexnum ]; //邻接矩阵
int vexnum,arcnum; //图的当前顶点数和边数
}MGraph;
无向图的度:第i行/列的非零元素个数
有向图: 入度-列 出度-行
p58
2.2 邻接表法(适用于稀疏图)
顺序+链式存储
2.3 十字链表(有向图)、邻接多重表(无向图)
p60
3.图的基操
4.图的遍历
4.1 广度优先遍历BFS
4.2 深度优先遍历
p63
十.查找
10.1 顺序查找(线性查找)
算法思想:从头到尾挨个找
有序表优化
被查概率不相等:被查概率大的放在靠前位置
int search_seq(sstable st, ElemType key){
st.elem[0]=key; //将下标为0的位置存放 key ,哨兵
int i;
for(i=st.length; st.elem[i] != key;--i);
return i;
}
10.2 折半查找(二分查找)
算法思想
- 仅适用于有序的顺序表。
时间复杂度:O( l o g 2 n log_2n log2n)
//升序
int search_Binary(sstable s, int key){
int low=0 , high = s.length-1, mid;
while(low <= high){
mid= (low+high)/2;
if(key == s[mid]){
return mid;
}else if(key < s[mid]){
high = mid-1;
}else{
low = mid +1;
}
}
return -1;
}
折半查找判定树是平衡二叉排序树
10.3 分块查找(索引顺序查找)
算法思想:
- 索引表中记录每个分块的最大关键字、分块的区间(起始,终止下标)
- 先查索引表(顺序或折半)、在对分块内进行顺序查找
10.4 二叉排序树(BST)
左子树结点值<根结点值<右子树结点值
//在二叉排序树中查找值为key的结点
//最坏空间复杂度O(1)
BSTNode * search(Tree T, ElemType key){
while(T.data != key && T != NULL){
if(T.data < key){
T = T.rchild;
}else{
T = T.lchild;
}
}
return T;
}
//二叉排序树插入
//二叉排序树构造
//二叉排序树的删除
10.5 平衡二叉树(AVL) p76
树上任意结点的左子树和右子树的高度之差不超过1.
调整最小不平衡子树
10.6 平衡二叉树的删除
p77
10.7 红黑树(RBT)
- 平衡二叉树存在的问题:插入/删除很容易破坏“平衡”特性
- 红黑树:插入/删除很多时候不会破坏“红黑”特性,无需频繁调整树的形态。即便需要调整,也可以在常数阶时间内完成。
- 以查为主-平衡二叉树
- 频繁插入删除,实用性强-红黑树
- 根结点是黑色的
- 叶结点(NULL)是黑色的
- 对每个结点,从该结点到任意叶结点的简单路径上,所含黑结点的数目相同
struct RBnode{
int key; //关键字的值
RBnode* parent; //父节点指针
RBnode* lchild; //
RBnode* rchild;
int color; //结点颜色
}
p79 插入
10.8 B树(多路平衡查找树)
m阶B树,一个结点m个指针
结点关键字个数n: ⌈ m / 2 ⌉ \lceil m/2 \rceil ⌈m/2⌉-1≤n≤m-1
绝对平衡,叶子结点在同一层
插入key后,若导致源节点关键字数超过上限,则从中间位置,将关键字分为两部分
插入到终端节点(最底层)
10.9 B+树 p83
结点的子树个数与关键字个数相等
支持顺序查找
10.10 散列查找(Hash)
散列表(Hash table /哈希表) :数据元素的关键字与其存储地址直接相关。
装填因子= 表中记录数/散列表长度
空间换时间
常见的散列函数
- 除留余数法: H(key)= key %p ,其中p是一个不大于表长但最接近m的质数。
- 直接定址法:H(key)= a*key+b :适合关键字的分布基本连续的情况
- 数字分析法
- 平方取中法
处理冲突的方法
- 拉链法(Java中采用)
- 开放定址法:
–线性探测法 : H i H_i Hi = (H(key) + d i d_i di)%m 其中i = 0,1,、、、k(k≤m-1)
–
–
十一.排序
指标
- 时间复杂度
- 空间复杂度
- 稳定性
分类
- 内部排序 :数据都在内存中
- 外部排序
1.插入排序
关键字,从后往前比。
时间复杂度 o( n 2 n^2 n2)
稳定
void InsertSort(int arr[], int len){
int i, j, temp;
for( i = 1; i < len; i++){
if(arr[i] < arr[i-1]){
temp = arr[i];
for(j = i-1; j >= 0 && arr[j] > temp; j--){
arr[j+1] = arr[j];
}
arr[j+1] = temp;
}
}
}
//优化 折半插入排序(仅仅适用于顺序表)
void InsertSort_Binary(int arr[], int len) {
int i, j, temp, low, high, mid;
for (i = 1; i < len; i++) { // 从第二个元素开始
temp = arr[i];
low = 0;
high = i - 1;
while (low <= high) {
mid = (low + high) / 2;
if (arr[mid] > temp) {
high = mid - 1;
} else {
low = mid + 1;
}
}
// 将元素向后移动
for (j = i; j > low; j--) {
arr[j] = arr[j - 1];
}
arr[low] = temp;
}
}
2.希尔排序(shell sort)
分子表,对子表排序。 每次分给子表的元素减少
仅适用于顺序表
void ShellSort(int arr[], int len){
int i, j, temp,d;
for(d = len/2;d>=1; d=d/2){
for( i = d; i < len; i+=2){
if(arr[i] < arr[i-d]){
temp = arr[i];
for(j = i-d; j >= 0 && arr[j] > temp; j--){
arr[j+d] = arr[j];
}
arr[j+d] = temp;
}
}
}
}
3.冒泡排序
O( n 2 n^2 n2)
是一种交换排序
void BubbleSort(int arr[], int len){
int i , j, temp;
for(i = 0; i < len-1 ; i++){
for(j = 0 ; j < len-i-1;j++){
if(arr[j] > arr[j+1]){
temp = arr[j+1];
arr[j+1] = arr[j];
arr[j] = temp;
}
}
}
}
//优化
void BubbleSort(int arr[], int len) {
int i, j, temp, swapped;
for (i = 0; i < len - 1; i++) {
swapped = 0; // 初始化交换标志位
for (j = 0; j < len - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
swapped = 1; // 发生了交换
}
}
// 如果没有发生交换,说明数组已经是有序的,可以提前结束
if (!swapped) {
break;
}
}
}
4.快速排序
- 选基准:首先,我们从待排序的数组中“挑”出一个元素来,这个元素我们叫它“基准”(pivot)。基准的选择很重要,但一开始我们可以简单地选择第一个元素或者最后一个元素作为基准。
- 分区:接下来,我们重新排列数组,把所有比基准小的元素都放到基准的左边,所有比基准大的元素都放到基准的右边。
- 递归排序:完成分区后,我们得到了两个子数组,一个是基准左边的数组(所有元素都比基准小),另一个是基准右边的数组(所有元素都比基准大)。然后,我们对这两个子数组分别重复上述的“选基准”和“分区”的过程,直到子数组的长度为0或1(也就是不需要再排序了)。
- 合并:但实际上,快速排序并不需要一个显式的合并步骤,因为在分区的过程中,数组就已经被“就地”(in-place)排序好了。每次分区都确保了基准元素被放到了它最终应该在的位置上,而左右两边的子数组也分别是有序的。
int partition(int arr[], int low, int high ){
int pivot = arr[low];//第一个元素为基准
while(low < high){
while(low < high && arr[high] >= pivot) --high;
arr[low] = arr[high];
while(low < high && arr[low] <= pivot) ++low;
arr[high] = arr[low];
}
arr[low] = pivot;
return low;
}
void quickSort(int arr[], int low , int high){
if(low < high){
int postion = partition(arr,low, high);
quickSort(arr,low, postion-1);
quickSort(arr,postion+1, high);
}
}
可以优化,选择pivot的方式。 如果选择 第一个元素为 pivot那么,在有序的情况下,时间复杂度高。
可以在 第一个元素,最后一个元素中,中间元素中选择 。
或者随机选择pivot。
在实际应用中,快速排序的时间复杂度接近O(n l o g 2 n log_2n log2n)
5.简单选择排序
是一种选择排序:每一趟在待排序元素中选取关键字最大或最小的元素加入有序子序列
void SelectSort(int arr[], int len){
for(int i=0;i<len-1;i++){
int min = i;
for(int j = i+1; j<len;j++){
if(arr[j]<arr[min]){
min = j;
}
}
if(min!=i){
int temp = arr[min];
arr[min] =arr[i];
arr[i] =temp;
}
}
}
6.堆排序(Heap) p92
大根堆:在逻辑上,完全二叉树中, 根节点值≥ 左右子树节点值
小根堆:根节点值≤ 左右子树节点值
建立大根堆
在顺序存储的完全二叉树中,非终端节点编号 i≤n/2 向下取整