数据结构三要素:逻辑结构、存储结构(物理结构)、数据运算

  1. 逻辑结构:元素之间的逻辑关系
    线性结构、非线性结构 || 集合、线性结构、树结构、网状结构
  2. 存储结构:数据在计算机中的表示(映像),也称物理结构
    顺序存储结构(顺序表)、链式存储结构(链表)、索引存储(索引表)、散列存储(散列表、哈希表);
  3. 数据运算:运算的定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤;

文章目录

  • CHAPTER1 线性表
    • 一、顺序表:用一组地址连续的存储单元依次存储线性表的元素;(如 数组)
    • 二、链表:单链表、双链表、循环链表
      • (一)单链表
      • 1.头插法建立单链表
      • 2.尾插法建立单链表
      • 3.约瑟夫环(Joseph Circle)(可运行)
      • (二)双链表
      • (三)循环链表
      • (四)静态链表
  • CHAPTER2 栈和队列
    • 一、栈
      • (一)顺序栈
      • (二)链栈
      • (三)栈的应用
    • 二、队列
      • (一)链队列
      • (二)顺序队列
      • (三)循环队列
      • (四)队列的应用
  • CHAPTER3 串
    • 一、朴素模式匹配算法
    • 二、KMP算法
  • CHAPTER4 数组和广义表
    • 一、数组
      • (一)数组
      • (二)数组的顺序表示
      • (三)矩阵的压缩存储
    • 二、广义表(重点)
      • (一)广义表的定义
      • (二)广义表的存储结构
  • CHAPTER5 树
    • 一、基本概念
      • (一)定义
    • 二、二叉树(必考)
      • (一)定义
      • (二)性质
      • (三)存储结构
      • (四)用二叉树表示表达式
    • 三、遍历二叉树和线索二叉树
      • (一)层序遍历---队列
      • (二)先中后序遍历(必考)
      • (三)线索二叉树(低概率考点)
    • 四、树、森林
      • (一)树的存储结构
      • (二)树、森林、二叉树的相互转换(必考)
      • (三)树、森林遍历
    • 五、哈夫曼树及应用
      • (一)定义
      • (二)哈夫曼算法构造最优二叉树
  • CHAPTER6 图
    • 一、定义
    • 二、图的存储结构
      • (一)图的数组表示法
      • (二)邻接表
    • 三、图的遍历(重要)
      • (一)DFS
      • (二)BFS
    • 四、图的应用(手动模拟)
      • (一)最小代价生成树-手动模拟
        • 1.**Prim算法求最小代价生成树**——不断加点的过程
        • 2.**克鲁斯卡尔求解最小生成树:**——不断加边的过程
        • 3.破圈法求解最小生成树
      • (二)拓扑排序
        • 1.AOV网
        • 2.拓扑排序-手动模拟
      • (三)关键路径-手动模拟
      • (四)最短路径-算法
        • 1.单源最短路径
        • 2.每对顶点间的最短路径
  • CHAPTER7查找
    • (一)静态查找
        • 平均查找长度ASL
      • 1.顺序查找
        • 1.1无序表的静态查找
        • 1.2有序表的静态查找
      • 2.折半查找 —算法
      • 3.分块查找(索引查找) —算法填空
    • (二)动态查找
      • 1.二叉排序树的定义(算法)
      • 2.二叉排序树上的查找运算及特点(算法)
      • 3.二叉排序树的创建构造(算法)
      • 4.二叉排序树的删除(不咋考-手动模拟即可)
      • 5.二叉平衡树的定义和构造(主要会旋转即可)
    • (三)哈希表(重中之重)
      • 1.定义
      • 2.散列函数的构造方法
      • 3.解决冲突的方法
      • 4.散列查找性能分析
  • CHAPTER8排序
    • (零)基本概念
    • (一)插入排序
      • 1.直接插入排序
      • 2.二分插入排序
      • 3.希尔排序
    • (二)交换排序
      • 1.冒泡排序
      • 2.快速排序
    • (三)选择排序
      • 1.简单选择排序
      • 2.树形选择排序
      • 3.堆排序
    • (四)归并排序
    • (五)分配排序

CHAPTER1 线性表

线性表:数据结构,是具有相同数据类型的n个数据元素的有限序列;

特点:

  • 存在惟一的第一个元素;
  • 存在惟一的最后一个元素;
  • 除第一个元素之外,每个元素均只有一个直接前驱;
  • 除最后一个元素之外,每个元素均只有一个直接后继;

有序表:对线性表中的数据元素按数据项递增或递减的顺序重新进行排列得到的线性表;仅描述元素之间的逻辑关系;

线性表的存储结构:

  • 顺序存储结构;顺序表
  • 链式存储结构:链表

一、顺序表:用一组地址连续的存储单元依次存储线性表的元素;(如 数组)

#include<iostream>
#include<cstdio>/**顺序表**///定义,动态分配
#define INIT_SIZE 100 //空间初始分配量
#define INCRECEMENT 10 //增量typedef struct{ElemType *elem;//存储空间的基址(指向动态分配数组的指针) 这个ElemType可以是任何一种数据类型 比如 int int length;//当前长度,元素个数 int listsize; //存储容量
}SqList; //插入
bool ListInsert(SqList &L,int i,ElemType e){/**①判断输入是否合法 */if(i<1||i>L.length+1)return false;/**②判断空间是否够用,否,扩展空间*/if(L.length==L.listsize){newbase=(ElemType*)realloc(L.elem,(L.listsize+INCRECEMENT)*sizeof(ElemType));//reslloc返回值是一个万能指针if(!newbase) return false;L.elem=newbase;L.listsize+=INCRECEMENT;     }  /**③插入元素*///从最后一个元素开始往后移,给新元素腾位置 for(int j=L.length;j>=i;j--){L.elem[j]=L.elem[j-1];}L.elem[i-1]=e;/**④善后处理 */L.length++;return true;
}//删除 (删除指定位置的元素,并将该值返回)
bool ListDelete(SqList &L,int i,ElemType &e){if(i<1||i>length)return false;e=L.elem[i-1];for(int j=i;j<length;j++){L.elem[j-1]=L.elem[j];}L.length--;return true;
}using namespace std;int main(){    return 0;
}

顺序表(数组)

顺序表的插入时间复杂度为:T(n)=O(n) ; 空间复杂度为S(n)=O(1);
顺序表的删除时间复杂度为:T(n)=O(n) ; 空间复杂度为S(n)=O(1);

二、链表:单链表、双链表、循环链表

(一)单链表
  • 每个结点只包含一个指针域,也称为线性链表;
  • 通常用头指针来标识一个单链表,如单链表L;
1.头插法建立单链表
  • 每个结点的插入时间为O(1),设单链表长为n,则总时间复杂度为O(n);
  • 实现了输入数据的就地逆置;
2.尾插法建立单链表
  • 增设尾指针r;
  • 生成的链表中结点数据与输入数据顺序一致;
  • T(n)=O(n);
#include<iostream>
#include<cstdio  >
#include<cstdlib>
using namespace std;//单链表定义
//这里struct后边跟了LNode是因为 在这个结构体中需要定义一个指针域
//不能等结构体定义完才给指针域定义类型,所以 struct后边跟了LNode
typedef struct LNode{ElemType data;//数据域struct LNode *next;//指针域 在LNode这个结构体没有取别名的情况下,必须通过struct LNode 来声明变量
}LNode,*LinkList; //LNode是结构体的对象,LinkList是指向结构体的指针 //头插法和尾插法都返回一个链表,即链表的地址L,则返回类型都为LinkList,指向结构体的指针
//声明变量时,都是用类型声明,所以*s,*r的类型为LNode,这也是为什么在结构体定义时,定义了一个LNode,有定义了一个*LinkList //头插法建立带头结点的单链表
LinkList List_HeadInsert(LinkList &L){LNode *s;int x;//创建头节点L=(LinkList)malloc(sizeof(LNode));//设置初始为空链表L->next=NULL;scanf("%d",&x);//输入9999表示输入结束 while(x!=9999){//创建新节点s=(LNode*)malloc(sizeof(LNode)); s->data=x;s->next=L->next;L->next=s;scanf("%d",&x);} return L;
} /*尾插法建立带头结点的单链表,头指针(L->next)不用置空:头指针的值赋值给了尾指针,尾指针会指向下一个结点,所以指向下一个结点之前,尾指针(头指针)到底指向哪里都不用管*/
//创建了头结点就必然会有头指针啊,不然怎么标识这个头结点呢?
//尾插法建立带头结点的单链表
LinkList List_TailInsert(LinkList &L){char ch;L=(LinkList)malloc(sizeof(LNode));LNode *s,*r=L; //r表尾指针 while((ch=getchar())!='\n'){s=(LNode*)mallo(sizeof(LNode));s->data=ch;r->next=s;r=s;}r->next=NULL;return L;
}
//尾插法建立不带头结点的单链表;不带头结点 但仍有头指针
LinkList List_TailInsertWithoutHead(){char ch;LinkList L;LNode *r,*s;//s指向新申请的结点L=NULL;r=NULL;while((ch=getchar())!='\n'){s=(LNode*)malloc(sizeof(LNode));s->data=ch;//进行判断,第一个结点的操作与其他结点不同 if(L==NULL)L=s;elser->next=s;r=s;} if(r->next!=NULL)r->next=NULL;return L;
} /**增删改查*///查找算法
//按序号查找结点的值,并返回该点的值和地址
LinkList FindList(LinkList L,int i,ElemType &e){int k=1;//计数器,记录这是查找到的第几个数,不然你知道查找到哪儿了?LNode *p=L->next;//if(i==0)return L;if(i<0)return NULL;//不存在序号为负的结点//p为最后一个结点或者已经找到第i个结点时停止循环 while(p&&k<i){p=p->next;k++; }e=p->data;return p;
} //按值查找
LinkList FindElem(LinkList L,ElemType &e){int j=1;//计数器LNode *p=L->next;while(p!=NULL&&p->data!=e){p=p->next;j++;}return p;}//在第i个位置 插入元素 后插
LinkList InsertElem(LinkList &L,int i,ElemType e){//找到待插入位置的前驱结点 LNode *p=L;int k=0;while(p!=NULL&&k<i-1){p=p->next;k++;}if(!p||k>i-1)return NULL;LNode *s=(LNode*)malloc(sizeof(LNode));if(!s) return NULL;s->data=e;s->next=p->next;p->next=s;return L;}//删除操作 按位序删除 并将删除的结点值返回
LinkList Delete(LinkList &L,int i,ElemType &e){//找到要删除结点的前驱结点 或可直接调用按位查找函数 LNode *p=L;int k=0;while(p&&k<i-1){p=p->next;k++}if(!p||k>i-1){return NULL;}LNode *q;//指向要删除的结点q=p->next;e=q->data;p->next=q->next;free(q);return L;
} int main(){return 0;
}

以上模板代码的具体实现:(可运行)

#include<iostream>
#include<cstdio>
#include<cstdlib>
using namespace std;typedef struct LNode{char data;struct LNode *next;
}LNode,*LinkList;//带头结点的尾插
LinkList InsertTailH(){char ch;LinkList L;L=(LinkList)malloc(sizeof(LinkList));LNode *s,*r=L;while((ch=getchar())!='\n'){s=(LNode *)malloc(sizeof(LNode));s->data=ch;r->next=s;r=s;}r->next=NULL;return L;}//按位序查找
LinkList find(LinkList L,int i,char &e){int k=1;LNode *p=L->next;if(i==0)return L;if(i<0)return NULL;while(p&&k<i){p=p->next;k++;}e=p->data;return p;
}//按值查找
LinkList find2(LinkList L,char e){int j=1;LNode *p=L->next;while(p!=NULL&&p->data!=e){p=p->next;j++;}cout<<j<<"this is what you want"<<endl;
}//在指定位置插入
LinkList InsertElem(LinkList &L,int i,char m){LNode *p=L;int k=0;while(p&&k<i-1){p=p->next;k++;}if(!p||k>i-1)return NULL;LNode *s=(LNode*)malloc(sizeof(LNode));s->data=m;s->next=p->next;p->next=s;return L;
} //删除指定位置的结点
LinkList Delete(LinkList &L,int i,char &h){LNode *p=L;int k=0;while(p&&k<i-1){p=p->next;k++;}if(!p||k>i-1)return NULL;LNode *q=p->next;h=q->data;p->next=q->next;free(q);return L;
}
int main(){LinkList L=InsertTailH();
//  char e='a';
//  find(L,3,e);
//  cout<<e<<endl;
//  find2(L,'w');InsertElem(L,2,'Z');find2(L,'Z');char h;Delete(L,2,h);cout<<h<<"已被删除"<<endl;return 0;
}
3.约瑟夫环(Joseph Circle)(可运行)

问题描述: 编号为1,2,…,n的n个人按顺时针方向围坐一圈,每人持有一个密码(正整数)。现在给定一个随机数m>0,从编号为1的人开始,按顺时针方向1开始顺序报数,报到m时停止。报m的人出圈,同时留下他的密码作为新的m值,从他在顺时针方向上的下一个人开始,重新从1开始报数,如此下去,直至所有的人全部出列为止。请编写程序求出圈的顺序。

分析:

  1. 每一个人代表一个数据元素,每个人有两个属性(数据项):编号和密码;
  2. 需要一个计数器k表示报到几了,当k与m相等时,删除该数据元素;
  3. 可以考虑顺序表或循环链表,因为要进行删除操作,选择循环链表简单一点;
  4. 考虑带尾指针的循环单链表:每次删除当前结点时,要通过当前结点的前一位来删除它,如果是头指针,那么就需要先循环到链尾再进行删除。
  5. 不带头结点的尾插法(显然不带头结点)
#include<iostream>
#include<cstdio>
#include<cstdlib>using namespace std;//Joseph Circle//定义单链表
typedef struct LNode{int No;//编号unsigned int Pwd;//密码struct LNode *next;
}LNode,*LinkList;//用尾插法创建一个结点数为n的不带头结点的单循环链表,返回值为尾结点的指针
LinkList Create_list(int n){LinkList r=NULL,p;//p:头指针 //创建循环单链表的第一个结点p=(LinkList)malloc(sizeof(LinkList));if(!p){cout<<"memory allocation error!"<<endl;return NULL;} cout<<"input the first password:"<<endl;cin>>p->Pwd;p->No=1;p->next=p;r=p;//循环创建其余n-1个结点for(int i=2;i<=n;i++){LNode *s=(LNode*)malloc(sizeof(LNode));if(!s){cout<<"memory allocation error!"<<endl;return NULL;} s->No=i;cout<<"input the "<<s->No<<"的password:"<<endl;cin>>s->Pwd;s->next=r->next;r->next=s;r=s;} r->next=p;return r;} //游戏玩法
void playing(LinkList tail,int n,int m){LinkList pre,p;//pre指向当前结点的前驱结点,p指向当前结点int k=1;//计数器//取余 毕竟每次都是一个循环 按最小的算 m=(m%n)?(m%n):n;pre=tail;//前驱结点指向链尾p=tail->next;//p指向第一个结点while(n>1){//圈中人数多于1时,循环 if(k==m){//循环到需要出圈的人时 cout<<m<<"   当前m的值"<<endl;printf("%4d即将出圈\n",p->No);pre->next=p->next;//删除n--;m=p->Pwd;m=(m%n)?(m%n):n;free(p);//释放当前结点(注意:不是释放p指针啊) p=pre->next;k=1;//下一轮循环仍然从1开始 } else{pre=p;p=pre->next;k++;}} printf("%4d是最后一个出圈的结点",p->No);
} int main(){LinkList tail;//带尾结点的单链表int n,ip;//玩家人数和初始密码cout<<"please input the number of players and initial password:" <<endl;cin>>n>>ip;//创建循环单链表tail=Create_list(n);if(tail){playing(tail,n,ip);} return 0;
}
(二)双链表

结点中有两个指针,prior和next,分别指向前驱和后继;

typedef struct DLnode{ElemType data;struct DLnode *prior,*next;
}DLnode,*DLinkList;
(三)循环链表

①循环单链表
最后一个结点的next指针指向头结点;

带尾指针的循环单链表的特点:

  • 通过表尾指针可以一步得到表头指针;
  • 对于简单的两个表的合并可以通过简单操作完成(T(n)=O(1));

②循环双链表
头结点的prior指针还要指表尾结点(即,某结点*p为尾结点时,p->next==L);
循环双链表为空表时,头结点的prior和next域都等于L(即,指向自身);

(四)静态链表

静态链表借助数组来描述线性表的链式存储结构,结点也有数据域和指针域,这里的指针是结点的相对地址(数组下标),又称游标。静态链表和顺序表一样需要预先分配一块连续的内存空间。

CHAPTER2 栈和队列

一、栈

栈:只允许在一端进行插入或删除操作的线性表;(操作受限的线性表)
后进先出(LIFO)

卡特兰数:n个不同元素进栈,出栈元素不同排列的个数为

(一)顺序栈

栈采用顺序存储结构
此时栈顶指针默认指向栈顶元素的下一位;

#include<iostream>
#include<cstdio>
#inclide<cstdlib>#define STACKINITIALSIZE 100
#define STACKINCRECEMENT 10 using namespace std;//栈的顺序存储表示
typedef struct{ElemType *base;//栈底指针,栈构造前和销毁后为空ElemType *top;//栈顶指针,指向栈顶元素的下一个位置int stacksize;//当前分配的栈的存储空间数
}SqStack;//构造空栈(初始化栈)
bool InitStack(SqStack &s){s.base=(ElemType*)malloc(STACKINITIALSIZE*sizeof(ElemType));if(!s.base)return false;s.base=s.top;s.stacksize=STACKINITIALSIZE;return true;
} //入栈
bool Push(SqStack &s,ElemType e){//判断是否栈满,栈满重新申请空间//top-base==stacksize(容量),说明栈已满 if((s.top-s.base)==s.stacksize){s.base=(ElemType*)realloc(s.base,(s.stacksize+STACKINCRECEMENT)*sizeof(ElemType));if(!s.base)return false;s.top=s.base+s.stacksize;s.stacksize+=STACKINCRECEMENT;}//元素入栈 *s.top=e;s.top++;return true;
} int main(){return 0;
}
(二)链栈

栈采用单链表存储
规定所有的操作都是在单链表的表头进行的,毕竟栈只能在一端进行操作


Q:是否需要另外设置尾指针?
A:否,尾结点的next------>NULL;可以判断尾结点在哪里,不需要另设;
Q:是否需要另设头结点?
A:否,此时栈顶指针top指向的是栈顶元素,入栈出栈操作可以在O(1)时间内完成,不需要设置头结点;
Q:建立链栈适合采用哪种插入法?
A:头插法;(头插法逆序)

typedef struct LinkStack{ElemType data;//数据域struct LinkStack *next;//指针域
}*LinkStack;

注意链栈元素出栈是e=top→data;

(三)栈的应用

主要是满足后进先出的特性

1.数制转换
43(10) = 101011(2)
思想:先求出来的余数放在后边

2.括号匹配
思想:自左至右扫描表达式,若遇左括号,则将左括号入栈,若遇右括号,则将其与栈顶的左括号进行匹配,若配对,则栈顶的左括号出栈,否则出现括号不匹配错误。

3.表达式求值(中缀表达式求值)
#优先级最低
思想:例如:4+2×3-10/5
按照运算法则,我们应当先算2×3然后算10/5 ,再算加法,最后算减法。
我们两个栈,一个用于存储运算符称之为运算符栈,另一个用于存储操作数称之为操作数栈
(1)首先置操作数栈为空,表达式起始符“#”为运算符栈的栈低元素。
(2)依次读入表达式中每个字符,若是操作数则进操作数栈,若是运算符则和运算符栈栈顶元素比较优先级,若栈顶元素优先级高于即将入栈的元素,则栈顶元素出栈(优先级高的先出栈,再把优先级低的放进来),操作数栈pop出两个操作数和运算符一起进行运算,将运算后的结果放入操作数栈,直至整个表达式求值完毕(即运算符栈顶元素和要放入元素均为“#”)

4.迷宫问题
思想:以栈S记录当前路径,则栈顶中存放的是“当前路径上最后一个位置信息”

  • 若当前位置“可通”,则纳入路径(入栈),继续前进;
  • 若当前位置“不可通”,则后退(出栈),换方向继续探索;
  • 若四周“均无通路”,则将当前位置从路径中删除出去。

5.递归调用

5.程序运行时的函数调用

二、队列

队列:
队列是仅限定在表尾进行插入和表头进行删除操作的线性表;
先进先出(FIFIO)

(一)链队列

队列的链式存储结构
默认带头结点;
实际上是一个同时带有队头指针和队尾指针的单链表;
适合于数据元素变动比较大的情况,不存在队满溢出的问题;

插入元素只动rear,删除元素只动front;

#include<iostream>
#include<cstdio>
#include<cstdlib>using namespace std;//队列的链式存储类型
//先定义链式队列结点,再定义链式队列
//先搞一颗珍珠
typedef struct QNode{QElemType data;struct QNode *next;
}QNode,*QueuePtr; //把珍珠串起来
typedef struct{QueuePtr front;//队头指针 QueuePtr rear;//队尾指针
}LinkQueue;//初始化;构造一个空队列Q ;带头结点哦~
bool InitQueue(LinkQueue &Q){Q.front=Q.rear=(QueuePtr)malloc(sizeof(QNode));if(!Q.front)return false;Q.front->next=NULL;return true;
} //判空
bool IsEmpty(LinkQueue Q){if(Q.front==Q.rear)return true;return false;
} //入队
bool EnQueue(LinkQueue &Q,QElemType e){//表面上插的是元素,实际上插入的是结点QueuePtr p=(QueuePtr)malloc(sizeof(QNode));if(!p)return false;p->data=e;p->next=NULL;Q.rear->next=p;Q.rear=p;return true;
} //出队
//若队列非空,对头元素出队并用e返回其值
bool DeQueue(LinkQueue &Q,QElemType &e){if(Q.front==Q.rear) return false;QueuePtr p=Q.front->next;Q.front->next = p->next;e=p->data;//若原队列中只有一个结点,删除后变空if(Q.rear==p)Q.rear=Q.front;//Q:这里写成Q.rear=NULL;也行吧 //A:不行!!!队列空的条件是front和rear都指向头结点return true;
}int main(){return 0;
}
(二)顺序队列

分配一块连续的存储单元存放队列中的元素,并附设两个指针,队头指针和队尾指针,队头指针指向队头元素,队尾指针指向队尾元素的下一个位置

空队列的条件:Q.front == Q.rear==0;

队满的条件:Q.rear==MaxSize

队不满但元素插入完毕:

队列长度:Q.rear-Q.front
对头元素:Q.base[Q.front]
队尾元素:Q.base[Q.rear-1]

顺序队列的假溢出:
队列中有空闲单元,但新元素进入队列无法在O(1)时间复杂度完成(需要移动元素)
解决办法:循环队列

(三)循环队列

循环队列:队列的顺序存储结构;
把存储队列元素的表从逻辑上视为一个环;

当Q.rear== Q.front时,如何区分队列空和队列满:
默认处理方式:
令队列空间中的一个单元闲置,使得在任何时刻,保持Q.rear和Q.front之间至少间隔一个空闲单元;实际上就是让判满的公式改变了一下,Q.rear+1 == Q.front;而队空是Q.rear==Q.front;

队列满: (Q.rear+1)%MAXSIZE == Q.front
队列空: Q.rear==Q.front
队列长度: (Q.rear - Q.front+ MAXSIZE)% MAXSIZE


循环队列

#include<iostream>
#include<cstdio>
#include<cstdlib>#define MAXSIZE 100using namespace std;//循环队列的存储结构
typedef struct{QElemType *base;int front;int rear;
}SqQueue;//初始化循环队列
bool InitQueue(SqQueue &Q){Q.base=(QElemType *)malloc(MAXSIZE*sizeof(QElemType));if(!Q.base)return false;Q.front=Q.rear=0;return true;
}//入队
bool EnQueue(SqQueue &Q,QElemType e){//将元素e插入队列Q的队尾if((Q.rear+1)%MAXSIZE==Q.front)return false;Q.base[Q.rear]=e;Q.rear=(Q.rear+1)%MAXSIZE;return true;
}//出队
bool DeQueue(SqQueue &Q,QElemType &e){//删除队列Q的队头元素并用e带回if(Q.front==Q.rear)return false;e=Q.base[Q.front];Q.front=(Q.front+1)%MAXSIZE;return true;
}int main(){return 0;
}
(四)队列的应用

1.层次遍历二叉树
2.解决主机与外设之间速度不匹配的问题:缓冲区
3.多用户资源竞争问题:CPU的分时

CHAPTER3 串

串:零个或多个字符组成的有限序列;
空串:长度为的串;
空白串:仅由一个或多个空格组成的串;
空串是任意串的子串,任意串是其自身的子串;

子串的定位运算成为模式匹配或串匹配;

串默认用顺序存储;

#include<iostream>
#include<cstdio>
#include<cstdlib>#define MaxStrLen 256; //预定义最大串长 using namespace std;//串的定长顺序存储表示
typedef struct{char ch[MaxStrLen];//每个分量存储一个字符 int length;//串的实际长度
}Sstring; //顺序串,求子串
//求串S从第POS个位置起,长度为len的子串sub
bool SubString(Sstring &sub,Sstring s,int pos,int len){//健壮性if(pos<1||pos>s.length-len+1||len<0)return false;//把主串的值赋给子串for(int i=pos;i<(pos+2);i++){sub.ch[i]=s.ch[i];} return true;
} int main(){return 0;
}

顺序串存储存在的问题:
空间大小固定,运算结果截断;

子串的定位运算又称为模式匹配或串匹配;

一、朴素模式匹配算法

思想:从主串、模式串(子串)的第一个位置开始比较(i=1,j=1),若相等,则 i,j 各自+1,然后比较下一个字符。若不等,主串指针回溯到上一轮比较位置的下一个位置,子串回溯到1,再进行下一次比较。(i=i-(j-1)+1)

#include<iostream>
#include<cstdio>
#include<cstdlib>#define MaxStrLen 256; //预定义最大串长 using namespace std;//串的定长顺序存储表示
typedef struct{char ch[MaxStrLen];//每个分量存储一个字符 int length;//串的实际长度
}Sstring; //朴素模式匹配 S:主串 T:子串
int Index(Sstring S,Sstring T){int i=j=0;while(i<=S.length&&j<=T.length){//在主、子串有效长度内 if(S.ch[i]==T.ch[j]){i++;j++;//继续比较后续字符 }else{//指针回溯 i=i-j+2;j=1;}}if(j>T.length)return i-T.length;//为啥是i-T.length而不是i-T.length+1;//因为最后一个相等之后,i,j还会执行一次自增操作 else return 0;//匹配失败
} int main(){return 0;
}


匹配成功的最好时间复杂度:O(m)

  • 刚好第一个就匹配上了,总对比次数为子串长度

匹配失败的最好时间复杂度:O(n-m+1)=O(n-m)=O(n)

  • 匹配成功之前,每一个与第一个字符都匹配失败;

最坏时间复杂度:O(nm-m^2+m)= O(nm)

  • 子串除了最后一个对不上,其余的都能对上,则每次遍历完一边后,又要走回头路;
  • 直到匹配成功/失败一共需要比较

m*(n-m+1)
m:每次需要移动m次
i需要移动n-m+1次

二、KMP算法

思想:失配时,只有模式串指针回溯,主串指针不变;

next数组求法(手动模拟):

  • 前1~j-1个组成串s
  • next[j]=s的最长相等前后缀长度+1
  • next[1]=0;
  • 若位序从0开始,next[j[整体-1

next[j]的含义:实际上是子串的下一个需要比较的位置;

//KMP算法
int Index_KMP(Sstring S,Sstring T,int next[]){int i=1,j=1;while(i<=S.length&&j<=T.length){if(j==0||S.ch[i]==T.ch[j]){i++;j++;}else{j=next[j];//发生失配时,模式串指针回溯 }if(j>T.length)return i-T.length;else return 0; }
}

求next[]的算法

//求next数组
//求模式串T的函数值,并存入数组next
int Get_Index(Sstring P,int next[]){int i=1,j=0;next[1]=0;while(i<=P.length){if((j==0)||P[i]==P[j]){i++;j++;if(P[i]!=P[j])next[i]=j;elsenext[i]=next[j];}elsej=next[j];}
}

CHAPTER4 数组和广义表

一、数组

这部分看王道就行

(一)数组
  • 数组是有n(>=1)个相同类型的数据元素构成的有限序列; 是线性表的推广;
  • 一维数组可以看作一个线性表,二维数组可以看作“数据元素是一维数组”的一维数组;
  • 三维数组可以看作“数据元素是二维数组”的一维数组;
(二)数组的顺序表示
(三)矩阵的压缩存储

1.特殊矩阵

2.稀疏矩阵
找规律算就行

二、广义表(重点)

(一)广义表的定义

广义表是线性表的推广。
L=(a1,a2,…,an ),n≥0,ai可以是单元素,也可以是一个表。

例如:
A = ( ):A是一个空表。
B = (e):B只有一个原子。
C = (a, (b,c,d) ):C有一个原子和一个子表。
D = (A, B, C):D有3个子表。
E = (a, E) = (a, (a, (a,…… , ) ):E是一个递归的表。

广义表 LS = ( a1, a2, …, an )的结构特点:
广义表中的数据元素有相对次序;
广义表的长度定义为表中的元素个数;
广义表的深度定义为表的嵌套层数;
注意:“原子”的深度为 0 ;
“空表”的深度为 1 。
广义表可以共享;
广义表可以是一个递归的表;
递归表的深度是无穷值,长度是有限值。


A = ( ):长度为0,深度为1。
B = (e):长度为1,深度为1 。
C = (a, (b,c,d) ):长度为2,深度为2 。
D = (A, B, C):长度为3,深度为3 。
E = (a, E) = (a, (a, (a, …… , ) ):长度为2,深度为∞ 。

任何一个非空广义表 LS = ( a1, a2, …, an) 均可分解表头和表尾两部分:
表头(Head):第一个元素
Head(LS) = a1
表尾(Tail):除第一个元素外其余元素构成的
Tail(LS) = ( a2, …, an)

D = ( E, F ) = ((a, (b, c)), F )
Head( D ) = ETail( D ) = ( F )
Head( E ) = a ; Tail( E ) = ( ( b, c) )
Head( (( b, c)) ) = ( b, c) ; Tail( (( b, c)) ) = ( )
Head( ( b, c) ) = b ; Tail( ( b, c) ) = ( c )

(二)广义表的存储结构

CHAPTER5 树

一、基本概念

(一)定义
  • 树:n个结点的有限集(树是一种递归的数据结构,适合于表示具有层次的数据结构)
  • 结点的度:一个结点的孩子个数
  • 树的度:树中节点的最大度数
  • 两结点之间的路径:由两个结点之间所经过的结点序列构成
  • 两结点之间的路径长度:路径上所经过的边的个数
  • 树的路径长度是指树根到每个结点的路径长的总和,根到每个结点的路径长度的最大值是树的高度减1

二、二叉树(必考)

(一)定义

每个结点至多有两棵子树,且二叉树的子树有左右之分,顺序不能颠倒;

(二)性质
  • 一个有n个结点的完全二叉树的高度H=[log(n)]+1
(三)存储结构

1.顺序存储
非完全二叉树不适合顺序存储
2.链式存储
二叉链表: 每个结点两个指针域;
三叉链表: 就多了个指向双亲结点的指针域;

(四)用二叉树表示表达式

按运算顺序构造二叉树,然后进行先中后序遍历即可;
对后缀表达式求值:遇到操作数就进栈,遇到操作符就从栈顶弹出两个操作数进行运算,最后运算结果入栈,循环至栈空;

三、遍历二叉树和线索二叉树

(一)层序遍历—队列

先根,后子树,先左子树,后右子树;某一结点出队后,将该结点的子树的根节点入队后,再将队头元素出队;

(二)先中后序遍历(必考)
#include<iosstream>
#include<cstdio>
#include<cstdlib>using namespace std;
//二叉树链式存储
typedef struct BiTNode{ElemType data;struct BiTNode *Lchild,*Rchild;
}BiTNode,*BiTree;//BiTree指向结构体的指针  BiTNode指向结构体的变量 //层序遍历
void Cengxu(BiTNode *root){InitQueue(Q);EnQueue(Q,root);while(!EmptyQueue){DeQueue(Q,p);//队头元素出栈 visit(p);EnQueue(Q,p->lChild);EnQueue(Q,p->rChild);}}//二叉树先序遍历(递归)
void PreOrder(BiTNode *root){if(root != NULL){cout<<root->data;PreOrder(root->Lchild);PreOrder(root->Rchild);}
}//中序遍历二叉树(递归)
void InOrder(BiTNode *root){//root指向根 if(root!=NULL){InOrder(root->Lchild);cout<<root->data;InOrder(root->Rchild);}
}//后序遍历二叉树(递归)
void PostOrder(BiTNode *root){if(root!=NULL){PostOrder(root->Lchild);PostOrder(root->Rchild);cout<<root->data;}
} //先序遍历(非递归) 这里的node就是BiTNode 懒得改了 非重点
void preOrder(Node root) {if(root==NULL) return null;Node* p=root;Stack<Node* > s; //建立一个栈 存储node类型 while(!s.empty() || p){ if(p){cout<<p->data; s.push(p); p=p->lchild; }else{p=s.top();s.pop();p=p->rchild; }
} }//中序遍历(非递归)
void InOrder_1(BiTNode *root){InitStack(S);//初始化栈 BiTNode *p;Push(&S,root);//根指针入栈While(!StackEmpty(S)){while(GetTop(S,p)&&p){Push(S,p->Lchild);//向左走到头 }Pop(S,p);//空指针退栈if(!StackEmpty(S)){Pop(S,p);cout<<p->data;//访问结点Push(S,p->Rchild);//向右 } }
}int main(){return 0;
}
(三)线索二叉树(低概率考点)


tag == 0:指向子树的根节点;
tag == 1:指向前驱或后继;
1.中序线索二叉树(非重点)
思路:在有左子树的情况下一直往左走,直到走到最左下的结点;

//中序线索二叉树上找指定结点的后继:BiThrTree  inordernext(BiThrTree p){if (p->rtag==1) return(p->Rchild);else  {q=p->Rchild;while (q->ltag==0)  q=q->Lchild;return(q);}}

2.后序线索二叉树(掌握算法)

思路:

  • 若p所指结点是整棵二叉树的根结点,则无后继结点;
  • 若p->Rtag=1,则p->Rchild指向其后继结点;
  • 若p->Rtag=0://P所指结点有右子树
    1.若p所指结点是其父结点f的右孩子,则其父结点f是其后继;
    2.若p所指结点是其父结点f的左孩子:
    ⅰ 若p所指结点没有右兄弟,则其父结点f是其后继;
    ⅱ 若P有右兄弟,则其后继结点是其父的右子树上后序遍历得到的第一个结点。
//在后序线索二叉树上查找指定结点的后继;BiThrTree  postorder_next(BiThrTree p){if (p->Rtag == 1)   return(p->Rchild);    else  {查找p所指节点的父结点f;if (p == f->Rchild)  return f;if (p == f->Lchild && f->Rtag == 1)   return f;q = f->Rchild;while (q->Ltag == 0 || q ->Rtag == 0) {if (q->Ltag == 0)q = q->Lchild;else q = q->Rchild;} return(q);
}
}

四、树、森林

(一)树的存储结构

1.双亲表示法:
采用一组地址连续的存储单元存储树的结点,通过保存每个结点的双亲结点的位置,表示树中结点之间的结构关系。(eg.a为根节点)

data parent
a 0

2.孩子表示法
通过保存每个结点的孩子结点的位置,表示树中结点之间的结构关系。
(类似于链表)

3.孩子兄弟表示法
用二叉链表存储树。链表的两个指针域分别指向该结点的第一个孩子结点和其右边的下一个兄弟结点。(左孩子,右兄弟)

(二)树、森林、二叉树的相互转换(必考)

工具:孩子兄弟表示法

(三)树、森林遍历

树的先根遍历等同于对转换所得的二叉树进行先序遍历
树的后根遍历等同于对转换所得的二叉树进行中序遍历

森林的先序遍历等于对转换所得的二叉树进行先序遍历
森林的中序遍历等于对转换所得的二叉树进行中序遍历

五、哈夫曼树及应用

(一)定义
  • 哈夫曼树(最优二叉树):带权路径长度最短的树;
  • 路径和路径长度:从树中的一个结点到另一个结点之间的分支构成这两个结点之间的路径,路径上的分支数目称作路径长度;
  • 结点的带权路径长度:从根到该结点的路径长度与该结点权的乘积称为结点的带权路径长度;
  • 树的带权路径长度:树中所有叶子的带权路径长度之和称为树的带权路径长度(WPL);

note:构建哈夫曼树时,都是两个两个合在一起的,所以没有度为一的结点,即n1=0;

(二)哈夫曼算法构造最优二叉树
  1. 在F中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左、右子树上根结点的权值之和;
  2. 在F中删除这两棵树,同时将新得到的二叉树加入F中;
  3. 重复1和2,直到F中只含一棵树为止。这棵树便是最优二叉树;

哈夫曼树适合采用顺序结构:已知叶子结点数n0,且n1=0,则总结点数为2n2+1(或2n0-1),且哈夫曼树构造过程需要不停地修改指针,用链式存储的话很容易造成指针偏移;

构造哈弗曼树的算法,会填空就行;

CHAPTER6 图

一、定义

  1. 图:顶点集和边集构成的二元组
  2. 分为无向图和有向图
  3. 无向完全图: 把能连起来的边都连起来:1+2+3+·····+n-1=n(n-1)/2
  4. 有向完全图:有来有回:n(n-1)
  5. 邻接点:边的两个顶点互为领接点
  6. 顶点V的度=与V相关联的边的数目(有向图中:度=入度+出度);图的所有顶点度数之和:2*e(e为边数)
  7. 路径:从一个点到另一个点所经过的顶点序列
  8. 网络(网):若图中的每条边都有权,这个带权图被称为
  9. 长度(无权图):沿路径所经过的边数成为该路径的长度;
  10. 长度(有权图):取沿路径各边的权之和作为此路径的长度;
  11. 简单路径:路径中的顶点不重复出现;
  12. 简单回路:由简单路径组成的回路;
  13. 连通图:无向图(有向图)中任意两个顶点之间都是连通的,称为连通图(强连通图);
  14. 连通分量:无向图G中的极大连通子图称为G的连通分量;对任何连通图而言,连通分量就是其自身;
  15. 强连通分量:针对于有向图;
  16. 生成树:一个连通图的生成树是一个极小连通子图,它含有图中全部顶点,但只有足以构成一棵树的n-1条边;(多加一条边就会形成一个环)

重要区分:
极大连通子图无向图的连通分量,极大即要求该连通子图包含其所有的边;非连通图(无向图)有多个极大连通子图,即多个连通分量;连通图(无向图)只有一个极大连通子图,即他本身;
极小连通子图:既要保持图连通,又要使得边数最少的子图;例如,生成树;

二、图的存储结构

图的存储结构至少要保存两类信息:顶点的数据和顶点间的关系;

(一)图的数组表示法

在数组表示法中,用邻接矩阵表示顶点间的关系

#include<iostream>
#include<cstdio>
#include<cstdlib>#define MaxVnum 50using namespace std;//图的数组表示法定义
//定义一个二维数组来表示邻接矩阵
typedef double AdjMatrix[MaxVnum] [MaxVnum];typedef struct{int vexnum,arcnum;//顶点数和边数AdjMatrix arcs;//邻接矩阵
}Graph;int main(){Graph G;  return 0;
}

数组表示法的特点:
//无向图

  1. 无向图的邻接矩阵是对称矩阵,同一条边表示了两次;
  2. 顶点v的度:等于二维数组对应行(或列)中值为1的元素个数;
  3. 判断两顶点v、u是否为邻接点:只需判二维数组对应分量是否为1;
  4. 顶点不变,在图中增加、删除边:只需对二维数组对应分量赋值1或清0;
  5. 设图的顶点数为 n ,用有n个元素的一维数组存储图的顶点,用邻接矩阵表示边,则G占用的存储空间为:n+n2;图的存储空间占用量只与它的顶点数有关,与边数无关;适用于边稠密的图;

//有向图

  1. 有向图的邻接矩阵不一定是对称的;
  2. 顶点v的出度:等于二维数组对应中值为1的元素个数;
  3. 顶点v的入度:等于二维数组对应中值为1的元素个数;
(二)邻接表

顶点:通常按编号顺序将顶点数据存储在一维数组中
关联同一顶点的边:用线性链表存储

网的邻接表表示:

#include<iostream>
#include<cstdio>
#include<cstdlib>#define MaxVnum 50using namespace std;//表结点结构
typedef struct ArcNode{int adjvex;double weight;struct ArcNode *nextarc;
}ArcNode;//头结点结构
typedef struct{VertexType data;ArcNode *firstarc;
}AdjList[MaxVnum]; //图
typedef struct{int vexnum,arcnum;AdjList vertexes;
}AGraph; int main(){AGraph G;return 0;
}

三、图的遍历(重要)

图的遍历:从图的某个顶点出发,访问图中的所有顶点,且使每个顶点仅被访问一次
深度优先搜索遍历(DFS)、广度优先搜索遍历(BFS);

(一)DFS

key points:递归、栈;类似于树的先序遍历

基本思想:

  1. 访问顶点v;
  2. 依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问;
  3. 若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。

手动模拟:
伪代码:

#include<iostream>
#include<cstdio>
#include<cstdlib>using namespace std;bool visited[MaxVnum];//访问标记数组//邻接表或者邻接矩阵存储图//DFS
void DFSTraverse(Graph G){for(int v=0;v<G.vexnum;++v)//vexnum:顶点数 visited[v]==false;//第一个for循环,初始化访问标记数组for(int v=0;v<vexnum;++v)//第二个for循环,从v=0开始对图进行DFS if(visited[v]==false)//此条件判断语句可计算图的连通分量个数DFS(G,v);
}
void DFS(Graph G,int v){visit(v);//访问顶点v;visited[v]=true;//设已访问标记 for(w为v的第一个领接点;w存在;w取v的下一个领接点)if(visited[w]==false)DFS(G,w);
}int main(){return 0;
}

算法执行过程图解:

复杂度分析:
遍历图的过程实质上是对每个顶点查找其邻接顶点的过程,所以耗费的时间取决于所采用的存储结构;
邻接链表表示:查找每个顶点的邻接点所需时间为O(e),e为边(弧)数,算法时间复杂度为O(n+e);
数组表示:查找每个顶点的邻接点所需时间为O(n2),n为顶点数,算法时间复杂度为O(n2);

(二)BFS

key points:队列,类似于树的层次遍历;

基本思想:从图中某顶点vi出发:

  1. 访问顶点vi ;
  2. 访问vi 的所有未被访问的邻接点w1 ,w2 , …wk ;
  3. 依次从这些邻接点(在步骤②中访问的顶点)出发,访问它们的所有未被访问的邻接点; 依此类推,直到图中所有访问过的顶点的邻接点都被访问;

tips:
为实现3,需要保存在步骤2中访问的顶点,而且访问这些顶点的邻接点的顺序为:先保存的顶点,其邻接点先被访问。

手动模拟:
首先从v1开始,v1入队,访问v1,visited[v1]=T;v1出队,v1的邻接点v2、v3入队,对v2、v3进行同等操作;

伪代码:

#include<iostream>using namespace std;void BFSTraverse(Graph G){for(v=0;v<G.vexnum;v++)visited[v]=false;//初始化标记数组InitQueue(Q);//初始化队列for(v=0;v<G.vexnum;v++){if(visited[v]==false){EnQueue(Q,v);visited[v]=true;while(!Empty(Q)){DeQueue(Q,u);//队头元素出队给Qfor(w取u的第一个邻接点;w存在;w取u的下一个邻接点){if(visited[w]==false){EnQueue(Q,w);visited[w]=true;}} }}}
}int main(){return 0;
}

复杂度分析:
同DFS

四、图的应用(手动模拟)

迪杰斯特拉、弗洛伊德掌握算法
prim、克鲁斯卡尔、破圈法,都是贪心的思想

(一)最小代价生成树-手动模拟

最小生成树的形式不是唯一的,但权值的和总是相同的;

为啥要求最小生成树:最小生成树是代价最小的,例如要在多个村庄之间修路,怎样使路径想通且代价最小,就应该考虑最小生成树;

求最小生成树所用到的性质:

最小生成树的MST性质:
假设G=(V,E)是一个连通网络,U是V中的一个真子集,若存在顶点u∈U和顶点v∈V-U 的边(u,v)是一条具有最小权的边,则必存在G的一棵最小生成树包括这条边(u,v)。

1.Prim算法求最小代价生成树——不断加点的过程

算法思想:
普里姆算法构造最小生成树的过程是从一个顶点U={u0}作初态,不断寻找与U中顶点相邻且代价最小的边的另一个顶点,扩充到U集合直至U=V为止。

注:

1.“与U之外的顶点 ”就保证了在构造最小生成树的过程中,不会有环形成;

  1. “扩充到U集合直至U=V为止”就保证了图中的所有顶点均被包含进来了;
    手动模拟:
    算法实现(顶多出填空题)
    要解决的问题:
  2. 顶点集合如何表示?–closedge
  3. 最小边如何选择?–lowcost里边非0 的最小边
  4. 一个顶点加入U集合如何表示?–令lowcost=0
//定义数组
/*
1.adjvex:V-U中的顶点,也是i的邻接点;
2.lowcost:当前顶点相连的最小代价的边
3.closedge[i].adjvex=k:表示从U和V-U中各选一个点,组成边(i,k)
4.顶点i加入U集合时:closedge[i].lowcost=0
*/
struct {int adjvex; double lowcost;
}closedge[MAX_VERTEX_NUM];void MiniSpanTree_PRIM (Graph G, VertexType u){//用普里姆算法从顶点u出发构造G的最小生成树for(j = 0; j < G.vexnum; ++j)  //辅助数组初始化if ( j != u ) closedge[j] = {u, G.arcs[u][j]};
closedge[u].lowcost = 0;        //初始,U={u}for(i = 1; i < G.vexnum; ++i) {k = minimum(closedge);  //求生成树的下一个顶点kcout << closedge[k].adjvex << G.vexs[k]; //输出生成树的边 closedge[k].lowcost = 0;      //顶点k并入U集合for(j = 0; j < G.vexnum; ++j)if (G.arcs[k][j] < closedge[j].lowcost)closedge[j] = {k, G.arcs[k][j]};
}

时间复杂度O(n2

2.克鲁斯卡尔求解最小生成树:——不断加边的过程

算法思想:

  1. 假设连通网N=(V,E),则令最小生成树的初始状态为只有n个顶点而无边的非连通图T=(V,{}),图中每个顶点自成一个连通分量
  2. 在E中选择代价最小的边,若该边依附的顶点落在T中不同的连通分量上,则将此边加入到T中,否则舍去此边而选择下一条代价最小的边。依次类推,直至T中所有顶点都在同一连通分量上为止。

手动模拟:

3.破圈法求解最小生成树

——思想简单,实现复杂
算法思想:每次选择最长的边进行删除,删除之后,保证图依然是连通的;

补:

AOV网 AOE网
顶点——活动 顶点——事件
有向边——活动的先后关系 有向边——活动,边的权值表示完成该活动的开销
侧重于表示活动的前后次序 除表示活动的先后次序外,还表示活动的持续时间
求解工程流程是否合理 解决工程所需最短时间及哪些子工程拖延会影响整个工程按时完成等问题
(二)拓扑排序
1.AOV网

AOV网:顶点表示活动表示活动的顺序关系的有向图称为AOV网;

特点:若在有向图中有弧<v,u>,则称顶点v是顶点u 的前趋,那么施工计划中顶点v 也排在u之前。也称u是v的后继。

一个AOV网不应该存在环,因为存在环意味着某项活动的进行应该以本活动的完成作为先决条件,会死锁。

手动快速输出拓扑序列:
每次都输出入度为0 的点,然后去掉这个点继续按这个规则输出;

2.拓扑排序-手动模拟
  1. 拓扑排序:将有向图中的顶点排成一个序列。
  2. 拓扑序列:有向图D的一个顶点序列称作一个拓扑序列。如果该序列中任两顶点v 、u ,若在D中v是u前趋,则在序列中v也是u前趋。

拓扑排序方法:

  1. 在有向图中选一个无前趋的顶点v,输出之;
  2. 从有向图中删除v及以v为尾的弧;
  3. 重复1、2,直接全部输出全部顶点或有向图中不存在无前趋的结点时为止;

算法不必掌握

(三)关键路径-手动模拟

AOE网:顶点表示事件,有向边表示活动,边上权值表示完成该活动的开销,称为用边表示活动的网络,即AOE网;
注:
事件Vi的最早发生时间是是源事件V1到Vi的最长路径长度;
活动的最早开始时间=弧尾事件的最早发生时间
活动的最晚开始时间=弧头事件的最晚发生时间-边的权值
边的权值即活动的持续时间

手动模拟: 掌握表格

(四)最短路径-算法

路径长度:路径上的边数、路径上边的权值之和

最短路径:两结点间权值之和最小的路径

求解最短路径的算法所依赖的性质:

两点之间的最短路径,也包含了路径上其他顶点间的最短路径;
(毕竟每一段最短,加起来才最短)

1.单源最短路径

即求图中某顶点到其他各顶点的最短路径:DJIKSTRA算法
算法思想:按路径长度递增顺序求解最短路径;本质:贪心
算法步骤:设V0是起始源点,S是已求得最短路径的终点集合

  1. V-S = 未确定最短路径的顶点的集合, 初始时 S={V0},长度最短的路径是边数为1且权值最小的路径
  2. 下一条长度最短的路径:
    ① Vi ∈ V - S ,先求出V0 到Vi 中间只经 S 中顶点的最短路径;
    ② 上述最短路径中长度最小者即为下一条长度最短的路径;
    ③ 将所求最短路径的终点加入S 中;
  3. 重复2直到求出所有终点的最短路径;

手动模拟: 会写表格
算法:——尽量掌握

2.每对顶点间的最短路径

方法一:每次以一个顶点为源点,重复执行迪杰斯特拉算法n次,求得每一对顶点之间的最短路径。

方法二:FLOYD算法——以邻接矩阵作为图的存储结构

手动模拟:
算法:——尽量掌握

CHAPTER7查找

(一)静态查找

平均查找长度ASL


p:查找到该元素的概率
c:比较次数

1.顺序查找

一般选择从后往前查找

1.1无序表的静态查找

将0号单元设置为“监视哨” 的目的是使得代码内的循环不必判断数组是否会越界,因为满足i==0时,循环一定会跳出,从而减少不必要的循环语句,进而提高程序效率;

#include<iostream>
#include<cstdio>
#include<cstdlib>using namespace std;typedef struct{KeyType key;OtherInfoType info;
}ElemType;typedef struct{ElemType *elem;//数据元素存储空间基址,建表时,按实际长度分配,0号单元留空int length;//表长
}SSTable;//在无序表中查找元素key所在的位置,查找成功则返回元素在表中的位置,否则返回0
int Sq_search(SSTable ST,KeyType key){int i=ST.length;ST.elem[0].key=key;//监视哨:下标为0的位置存放待查找的元素while(ST.elem[i].key!=key) i--;return i;
}int main(){return 0;
}

——查找失败只有一种可能性:走到监视哨了;

1.2有序表的静态查找
#include<iostream>
#include<cstdio>
#include<cstdlib>using namespace std;typedef struct{KeyType key;OtherInfoType info;
}ElemType;typedef struct{ElemType *elem;//数据元素存储空间基址,建表时,按实际长度分配,0号单元留空int length;//表长
}SSTable;//假设表中元素按递增排序;查找成功时返回下标;失败时返回0
int Sq_search(SSTable ST,KeyType key){int i=n;ST.elem[0].key=key;//监视哨:下标为0的位置存放待查找的元素while(ST.elem[i].key>key) i--;if(ST.elem[i]==key){return i;} return 0;
}int main(){return 0;
}

ASL(成功)和无序表一样;
查找失败:n个元素,就由n+1个空隙,即n+1种出错的可能;这里从代码就可看出,若key大于最大值或小于最小值,都是查找失败,所以就有n+1种出错的可能;在n+1处查找失败要比较1次,在第n个和第n-1个元素之间失败要比较两次;

2.折半查找 —算法

折半查找的前提:一定得有序;
mid=[low+high]/2(向下取整)

算法思想:
先确定待查记录所在的范围(区间),若待查记录等于表中间位置上的记录,则查找成功;否则,缩小查找范围,即若待查记录小于中间位置上的元素,则下一次到前半区间进行查找,若待查记录大于中间位置上的元素,则下一次到后半区间进行查找。

#include<iostream>
#include<cstdio>
#include<cstdlib>using namespace std;typedef struct{KeyType key;OtherInfoType info;
}ElemType;typedef struct{ElemType *elem;//数据元素存储空间基址,建表时,按实际长度分配,0号单元留空int length;//表长
}SSTable;//在有序表中查找元素e,查找成功时返回下标;失败时返回0
//若是该数组中没有这个元素,最后都会产生low>high;这就是循环退出条件;
int B_search(SSTable ST,KeyType key){int low=1,high=ST.length;while(low<=high){mid=(low+high)/2;// “/”本就是向下取整 if(ST.elem[mid].key==key) return mid;else if(ST.elem[mid].key<key) low=mid+1;else high=mid-1;}return 0;//查找失败
}int main(){return 0;
}

note:链表只能用顺序查找,毕竟这玩意不能随机存储;

折半查找判定树:
树中每个圆形结点表示一个记录,结点中的值为该记录关键字的值,树中最下面的结点都是方形的,表示查找不成功的情况
结点在第几层,查找成功就需要比较几次;
查找成功时的查找长度为从根节点到目的结点的路径上的结点数;
查找失败时的查找长度为从根节点到对应失败结点的父结点的路径上的结点数;

每个根节点都是mid

在失败的情况下,最后的mid,high,low都指向同一个结点,如果mid已经不等的话就已经失败了,即到a1,a3,a5,a7.a8的位置就知道失败了,不会再继续向下比较了;所以失败时的比较次数应该是叶子结点的父结点的比较次数;然后,n个结点有n+1种失败的可能;
补充:比较到方框时,就一定有low>high,不会再继续while循环了;

查找成功和失败的平均查找长度与有n个结点的完全二叉树的高度相同。

3.分块查找(索引查找) —算法填空

  1. 先在索引表中确定元素所在的块;
  2. 再在块中顺序查找;
    ASL(分块查找)=ASL(索引表内的查找)+ASL(块内查找)

(二)动态查找

就是可以对表进行增删改查

1.二叉排序树的定义(算法)

注:二叉排序树出手动模拟的概率比较大!

具有以下性质的二叉树:

  1. 若其左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  2. 若其右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  3. 其左、右子树也分别为二叉排序树;
    即:左小右大
2.二叉排序树上的查找运算及特点(算法)
BiTree SearchBST(BiTree T,keyType key) {//在T指向根的二叉排序树中递归地查找关键字等于key的数据元素,
//若找到,返回指向该结点的指针,否则返回NULLif (T==NULL) return NULL;else if (T->data.key==key) return T;else if (key < T->data.key)return SearchBST(T->lchild,key);else return SearchBST(T->rchild,key);
}
3.二叉排序树的创建构造(算法)

步骤:

  1. 若二叉排序树为空树,则插入元素作为树根结点;
  2. 若根结点的键值等于key,则插入失败;
  3. 若key小于根结点的键值,则插入到根的左子树上;否则,插入到根的右子树上;

新插入的结点一定是个叶子结点;

二叉排序树结点的值一定是唯一的;且插入的新值,一定是插入在为空的位置上;

算法如下:
二叉排序树的特点:

  • 将一个无序序列的元素依次插入到一棵初始为空的二叉排序树上,然后进行中序遍历,可得到一个有序序列。(默认是升序)
  • 在二叉排序树上插入元素时,总是将新元素作为叶子结点插入,插入过程中无须移动其他元素,仅需将一个空指针修改为非空。
  • 在二叉排序树上删除元素时,应保持其中序遍历序列有序的特点。
4.二叉排序树的删除(不咋考-手动模拟即可)

删除时的三种情况:

  1. 待删除的结点p是个叶子结点
    p是f的左孩子将p的父结点f的左孩子指针置空,若p是f的右孩子,则将f的右孩子指针置空;(即直接删)
  2. 待删除的结点p是仅有一个非空子树
    (那么它中序遍历的前驱(后继)就是其左孩子(右孩子);
    用前驱或后继覆盖;)

    则将
    p的左孩子(或右孩子)接为p的父结点f的左孩子(若p是f的左孩子),或者将p的左孩子(或右孩子)接为p的父结点f的右孩子(若p是*f的右孩子) ;
  3. 待删除的结点p有两个非空子树
    (p的直接前驱就是左子树的最右边(即该结点是没有右子树的单枝树),直接后继就是右子树的最左边(即该结点是没有左子树的单枝树;不然咋叫最左边、最右边呢);这样删除两个子树都不为空的结点就转化为了删除只有一个子树或叶子结点的情况;
    本质上就是按二叉排序树的性质,把另一部分拼上去;

    a.令
    p的直接前驱(或直接后继)代替p,然后再删除其直接前驱(或直接后继)

    b.令
    p的左子树为f的左子树(若p是f的左孩子),而p的右子树为s的右子树(s是对p的左子树进行中序遍历的最后一个结点);

    或令p的右子树为f的右子树(若p是f的右孩子),而p的左子树为*s的左子树(s是对p的右子树进行中序遍历的第一个结点);

二叉排序树的查找性能:
求ASL和二分的查找树一样;

若查找成功,则走了一条从根结点到某结点的路径,若查找失败,则走到一棵空的子树时为止。因此,最坏情况下,其平均查找长度不会超过树的高度。

高度越高平均查找长度就越大;

5.二叉平衡树的定义和构造(主要会旋转即可)

如果根据关键字的输入序列构造的二叉树为单枝树,则其平均查找长度与顺序查找相同,因此在构造二叉排序树的过程中需要进行处理,使得树中结点的分布比较均匀。因此就有了平衡二叉树的概念;

定义:平衡二叉树或者是一棵空树,或者是具有下列性质的二叉树:它的左子树和右子树都是平衡二叉树,且左、右子树的深度之差的绝对值不超过1。

平衡因子:定义二叉树中结点的平衡因子bf等于结点左子树的高度减去右子树的高度;平衡二叉树中结点的平衡因子为0、1或-1。

四种平衡处理:

  1. RR型左旋平衡处理

  2. LL型右旋平衡处理

  3. LR型先左后右旋转平衡处理

  4. RL型先右后左旋转平衡处理
    补充:文件系统底层用的B树

(三)哈希表(重中之重)

1.定义

散列:也称为杂凑或哈希。它既是一种查找方法,又是一种存储方法,称为散列存储,其内存存放形式也称为哈希表散列表

处理冲突:
一般情况下,设计出的散列函数很难是单射的,即不同的关键字对应到同一个存储位置,这样就造成了冲突(碰撞)。此时,发生冲突的关键字互为同义词

2.散列函数的构造方法

直接定址法、数字分析法、平方取中法、折叠法、随机数法、除留余数法
几乎只考 除留余数法

  1. 直接定址法
    可表示为H(key)=a*key+b,其中a、b均为常数;
    这种方法计算特别简单,并且不会发生冲突,但当关键字分布不连续时,会出现很多空闲单元,会将造成大量存贮单元的浪费;

  2. 数字分析法
    分析关键字的各个位的构成,截取其中若干位作为散列函数值,尽可能使关键字具有大的敏感度;

  3. 平方取中法
    这种方法是先求关键字的平方值,然后在平方值中取中间几位为散列函数的值。因为一个数平方后的中间几位和原数的每一位都相关,因此,使用随机分布的关键字得到的记录的存储位置也是随机的。

  4. 折叠法
    将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为散列函数的值,称为折叠法;
    例如,假设关键字为某人身份证号码430104681015355,则可以用4位为一组进行叠加,即有5355+8101+1046+430=14932,舍去高位,则有H(430104681015355)=4932。

  5. 随机数法
    给随机数呗

  6. 除留余数法
    Hash(key) = key % p
    P一般取小于表长的最大质数;

除留余数法的关键是选取较理想的p值,使得每一个关键字通过该函数转换后映射到散列空间上任一地址的概率都相等,从而尽可能减少发生冲突的可能性。一般情形下,取p为一个素数较理想。

3.解决冲突的方法
  1. 开放定址法
    开放定址法就是从发生冲突的那个单元开始,按照一定的次序,从哈希表中找出一个空闲的存储单元,把发生冲突的待插入关键字存储到该单元中,从而解决冲突。

H(key)=key%p:这个p,是为了找到散列地址;
Hi = (H(key)+di) MOD m i=1,2,…K(K<=m-1);
m为散列表长度,类似于循环队列,超出表长以后就循环到最左边;
di为增量序列,是指发生第i次冲突的时候,H(key)往后偏移了多少位;

1.1di的取法

a. 线性探测再散列 di = 1, 2, 3, …
线性探测再散列法充分利用了哈希表的空间,但在解决一个冲突时,可能造成新的冲突(聚集)。另外,也不能随便对结点进行删除。

更正:散列表的表长m应该为16,即数组下标为0~15;(非图中所示,自己算算就行)

b. 二次探测再散列 di = 1, -1, 22, -22

c. 伪随机探测再散列 di是伪随机数
就每次都加随机数

  1. 链地址法
    链地址法也称拉链法,是把相互发生冲突的同义词用一个单链表链接起来,若干组同义词可以组成若干个单链表。

  2. 再哈希法(略)

  3. 公共溢出区法(略)

4.散列查找性能分析

装填因子:所谓装填因子是指散列表中存入的元素个数n哈希表的大小m的比值,装填因子α =n/m

若题中只给了装填因子,则此时的平均长度为:
1.线性探查法的查找性能分析
若是具体的题目:
其中的1,2,3之类的,是指比较了多少次才插入成功;
因为表里边有多少个数,查找成功的概率就是几分之一;
失败的概率是1/p,因为模p,就会有0~p-1总共p种结果;

2.链地址法的查找性能分析
查找成功: 查找14需要进行一次比较,查找1需要进行2次比较,查找27需要进行3次比较,综上,找到在第一层的数只需要比较一次,找到在第二层的数需要比较两次,类推;
查找失败: 对于地址1,比较四次后确定元素不在表中,对于地址2,比较两次即可,类推;

CHAPTER8排序

——内部排序

(零)基本概念

排序: 将一个数据元素的任意序列重新排列成一个按关键字有序的序列;

内部排序: 待排序的记录存放在计算机的内存中所进行的排序操作称为内部排序;

外部排序: 待排序的记录数量很大,以致内存一次不能容纳全部记录,在排序过程中需要访问外存的排序过程称为外部排序;

稳定的排序: 比如一个序列是“1,4,3,3*,2”,按从小到大排序后变成“1,2,3,3* ,4”,就叫做稳定排序,即3和3*相对顺序不变;

内部排序的算法性能取决于算法的时间复杂度和空间复杂度,而时间复杂度一般是由比较和移动次数决定的;

大部分排序算法都仅适用于顺序存储的线性表;

(一)插入排序

1.直接插入排序

基本思想: n个待排序的元素由一个有序表和一个无序表组成,开始时有序表中只包含一个元素。排序过程中,每次从无序表中取出第一个元素,将其插入到有序表中的适当位置,使有序表的长度不断加长,完成排序过程。(打过斗地主吧?一样的)

void InsertSort(SqList &L){//一定是从2开始,因为0是监视哨,这里用来暂时存放待比较元素
//1是有序表的第一个元素
for(int i=2;i<=L.length;++i)if(L.r[i].key<L.r[i-1].key){L.r[0]=L.r[i];L.r[i]=L.[i-1];for(int j=i-2;L.r[0].key<L.r[j];--j)L.r[j+1]=L.r.[j];L.r[j+1]=L.r[0];//[j]<[0],把[0]放在[j+1]}
}

最好情况下(正序)
元素的比较次数为: n - 1
元素的移动次数为:0

n-1:从第二个数开始依次跟前一个数比较一次,总共就n-1次呗;
不移动元素;

最坏情况下(逆序)
元素的比较次数: (n+2)(n-1)/2
元素的移动次数为: (n+4)(n-1)/2

注: 每次比较到最后,都会在“哨兵”的位置自身比较,当然了,这个特性也是哨兵的自身特性吧,用于防止溢出,在这里就用来跳出最内层的for循环;
(n+2)(n-1)/2:2+3+……+n

注: 移动次数要包括把i所指向的当前元素放入哨兵位置
(n+2)(n-1)/2:3+4+………+n+1

总的比较次数和移动次数皆约为 n2 /4,则直接插入排序的时间复杂度为O(n2);

空间复杂度:O(1),只需要1个辅助单元,就那个哨兵;

适用于元素数目少,或者元素的初始序列基本有序;

2.二分插入排序

在寻找插入位置时采用二分查找,则称为折半插入排序;

void   InsertSort(SqList &L)   /* 对顺序表L作折半插入排序 */
{   for(i=2;i <= L.length; i++) {   L.r[0] = L.r[i];                /* 保存待插入元素 */low = 1;high = i-1;    /* 设置初始区间 */while(low <= high)       /* 确定插入位置 */{   mid = (low+high)/2;if (L.r[0].key > L.r[mid].key)low = mid+1;      /* 插入位置在高半区中 */else  high = mid-1;   /* 插入位置在低半区中 */}/* while */for(j = i - 1; j >= high +1;  j--) /* high+1为插入位置 */L.r[j+1] = L.r[j];          /* 后移元素,留出插入空位 */L.r[high+1] = L.r[0];      /* 将元素插入 */}/* for */
}/* InsertSort */

折半插入排序仅减少了比较元素的次数,该比较次数与待排序表的初始状态无关,仅取决于表中的元素个数n,而元素的移动次数并未改变,它依赖于待排序表的初始状态;
时间复杂度仍为O(n2);

3.希尔排序

算法思想: 先将整个待排序元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的),分别进行直接插入排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。

void ShellInsert(SqlList &L, int dk){for(int i=dk+1; i<=L.length;i++){//把所有组都遍历一遍if(L.r[i].key<L.r[i-dk].key){L.r[0]=L.r[i];for(int j = i-dk; j>0&&L.r[0].key<L.r[j].key; j-=dk){L.r[j+dk]=L.r[j];//后挪}L.r[j+dk]=L.r[0];//插入}}
}

注:希尔排序并不是一次性把一个组都遍历完;
建议跟着代码单步执行,并不是一次就把A、D、G排完的;9
最终增量dk的值一定是1,不然没办法把整个序列都排序;

(二)交换排序

1.冒泡排序

————算法题;会写
没有发生交换时,表明表已有序,冒泡排序结束;
i=1表示第一趟,在每一趟中,只要进行了一次交换,change都会变为true;

void BubbleSort(SqlList &L){for(int i=1,change=true;i<L.length&&change;++i){change=false;for(int j=1;j<L.length-i+1;++j){if(L.r[j].key>L.r[j+1].key){//看,这里比较的是j和j+1,所以循环控制条件是小于,不是小于等于L.r[0]=L.r[j];L.r[j]=L.r[j+1];//往前挪L.r[j+1]=L.r[0];change=true;}//if}//for}//for
}//BubbleSort

每趟冒泡的结果是把序列中的最大元素(或最小元素)放到了序列的最终位置,这样最多做n-1趟冒泡就能把所有元素排好序;

n-1 是因为,把前n-1个最大(小)的放到了最终位置上,剩下的最后一个,自然在最终位置上了;

最好的情况下,元素比较次数为:n-1(这个n-1是指第二个for循环中的j从1~n-1);
最坏情况下(全为逆序),比较次数为n(n-1)/2,每一个元素都要和剩下的(n-i)个元素各比较一次,(n-1)+(n-2)+……+2+1=n(n-1)/2;

最好情况下,元素的交换次数为0;
最坏情况下,元素的交换次数为:n(n-1)/2;还是全为逆序,每一个元素都要和后边的n-i个元素各交换一次;

空间复杂度O(1);

2.快速排序

——手动模拟
快排是目前所有内排序算法中速度最快的一种;
算法思想:
取待排序序列中的某个元素作为基准(一般取第一个元素),通过一趟排序,将待排元素分为左右两个子序列,左子序列元素的关键字均小于或等于基准元素的关键字,右子序列的关键字则大于基准元素的关键字,然后分别对两个子序列继续进行排序,直至整个序列有序。

说人话版:先挑个值做标杆(这里选择第一个元素),把标杆放入pivot,比标杆小的放左边,大的放右边;放的过程有讲究,初始时,令i指向序列最右边的值,j指向序列最左边的值,然后从j开始,当遇到比标杆小的值,就放入当前i所指的位置,然后j不动,i++,若i所指向的值比标杆大,则放入当前j所指向的位置,否则i++;直到遇到比标杆大的;对子序列进行同样的操作即可;

过程:






手动模拟请掌握:


快速排序是对冒泡排序的一种改进方法,算法中元素的比较和交换是从两端向中间进行的,排序码较大的元素一次就能够交换到后面的单元,排序码较小的记录一次就能够交换到前面的单元,记录每次移动的距离较远,因而总的比较和移动次数较少。

当一个序列恰好是逆序(正序)时,快速排序就退化成了冒泡排序;

(三)选择排序

选择排序的基本思想:
每一趟(如第i趟)在后边n-i+1(i=1,2,……,n-1)个待排元素中选取关键字最小的元素,作为有序子序列的第i个元素,直到第n-1趟做完,待排元素只剩下一个,就不用再选了;

1.简单选择排序

——不稳定
基本思想是:
第一趟在n个记录中选取最小记录作为有序序列的第一个记录,第二趟在n-1个记录中选取最小记录作为有序序列的第二个记录,第i趟在n-i+1个记录中选取最小的记录作为有序序列中的第i个记录。

void SelectSort(SqList &L)  {//对顺序表作简单选择排序for(i = 1; i < L.ength; i++) {for(k = i, j =i; k <= L.length; k++)if  (L.r[k].key < L.r[j].key) j = k;//每次就保存最小的那个下标if (j != i) {L.r[i] ← → L.r[j];} //把最小的和无序序列的第一个进行交换} //for
} // SelectSort
2.树形选择排序

又称锦标赛排序(Tournament Sort):

  1. 首先对n个记录的关键字两两进行比较,然后在n/2个较小者之间再进行两两比较,如此重复,直至选出最小关键字的记录。整个过程可用一个含有n个叶结点的二叉树表示;
  2. 选出最小记录后,将树中的该最小记录修改为∞,然后从该叶子结点所在子树开始,修改到达树根的路径上的结点;



    缺陷太大,一般不考虑;
3.堆排序

只需要一个元素的辅助空间
算法的时间复杂度为O(nlogn)

分为大根堆、小根堆;

堆是采用顺序表来存储的一个完全树;

堆排序的存储就是它本身,不需要额外的存储空间;要么只需要一个用于交换或临时存放元素的辅助空间;

从定义能推出来,堆一定是完全二叉树。不然怎么能用2i表示左孩子,2i+1表示右孩子呢;

建初始堆,一定是从序号为n/2(向下取整)的元素开始建,因为这能保证它一定有父结点;

(四)归并排序

(五)分配排序

数据结构学习笔记(考研 笔记 完结 西电)相关推荐

  1. 考研数据结构学习与总结笔记---1.1数据结构的基本概念

    考研数据结构学习笔记---王道 数据结构的基本概念 1.数据 2.数据元素 3.数据对象 4.数据类型 5.数据结构 数据结构三要素 1.数据的逻辑结构 2.数据的存储结构 3.数据的运算 回顾与总结 ...

  2. 数据结构学习(考研408)

    112 目录 其他 开端 线性表 栈和队列 栈 队列 队列和栈的应用 矩阵的压缩存储 串 KMP算法 树 相关概念术语 二叉树 二叉树的遍历 树与森林 树的应用 图 图的相关概念 图的存储 图的遍历: ...

  3. 西电计算机考研历年分数线,西电历年考研分数线

    西电历年考研分数线?细节决议胜败,这是很多人都明白的道理,但详细到咱们的行动上可即是另一码事了.在高考的道路上,分数决议一切的思想在考生们心里根深柢固.但是咱们可知道,除了分数,很多填写志愿上的细枝末 ...

  4. 【考研复试】西电833/834计网笔记

    [考研复试]833/834计网笔记 分层可以带来如下好处 OSI五层模型 应用层 运输层 数据链路层 物理层 数据链路层 为什么要划分子网 局域网 虚电路服务与数据报服务的主要区别 IP层处理数据报的 ...

  5. 西电计算机考研833试题,西电考研辅导班:西安电子科技大学833“计算机学科专业基础综合”复习参考提纲...

    一.考察目标 计算机学科专业基础综合考试涵盖数据结构和计算机组织与体系结构等学科专业基础课程.要求考生比较系统地掌握上述专业基础课程的基本概念.基本原理和基本方法,能够综合运用所学的基本原理和基本方法 ...

  6. 考研数据结构学习笔记1

    考研数据结构学习笔记1 一.绪论 1.基本概念和术语 2.数据结构三要素 2.1逻辑结构 2.1.1 集合结构 2.1.2 线性结构:一对一 2.1.3 树形结构:一对多 2.1.4 图状结构:多对多 ...

  7. 考研[*数据结构*]学习笔记汇总(全)

    文章目录: 一:预备阶段 二:基础阶段笔记 三:冲刺阶段笔记 四:各章节思维导图 五:题库 来源:王道计算机考研 数据结构 一:预备阶段 之前的数据结构笔记 数据结构--学习笔记--入门必看[建议收藏 ...

  8. 数据结构学习笔记——顺序表的基本操作(超详细最终版+++)建议反复看看ヾ(≧▽≦*)o

    目录 前言 一.顺序表的定义 二.顺序表的初始化 三.顺序表的建立 四.顺序表的输出 五.顺序表的逆序输出 六.顺序表的插入操作 七.顺序表的删除操作 八.顺序表的按位和按值查找 基本操作的完整代码 ...

  9. 数据结构 - 学习笔记 - 红黑树

    数据结构 - 学习笔记 - 红黑树 定义 简介 知识点 1. 结点属性 2. 前驱.后继 3. 旋转 查找 插入 父结点为黑色 父结点为红色 1. 有4种情形只需要变色(对应234树4结点) 1.1. ...

  10. 谷粒商城商品规格数据结构学习笔记(SPUSKU)

    谷粒商城商品规格数据结构学习笔记(SPU&SKU) SPU Standard Product Unit (标准产品单位) ,一组具有共同属性的商品集 SKU Stock Keeping Uni ...

最新文章

  1. 第七篇T语言实例开发,文本与程序的几种打开方法(版5.3)
  2. MySQL 复制 - 性能与扩展性的基石:概述及其原理
  3. glassfish_重写到边缘–充分利用它! 在GlassFish上!
  4. mysql连接代替子查询_MySQL优化之使用连接(join)代替子查询
  5. [Ural1099]工作安排 带花树
  6. c语言不能正常输出128以上的ascii字符,C语言输出ASCII 219无法显示方格
  7. “去哪儿”创始人兼总裁庄辰超专访实录
  8. php 网站攻击,php网站主要攻击方式
  9. flink sql 报错:FlinkRuntimeException: Exceeded checkpoint tolerable failure threshold
  10. layim之邀请好友加入群组
  11. 树莓派mongodb实战
  12. supervisor 进程托管程序的使用示例
  13. Keystore、Key attestation
  14. 15.全文检索-ElasticSearch
  15. kali Linux更新软件包
  16. win10安装wsl 2.0子系统 安装在非C盘
  17. 智能消息服务-数字短信使用FAQ简介: 数字短信是基于普通短信,通过一套编码技术实现的短信服务。它能把视频、音频、网页、GIF图、文字等多种表现形式通过多媒体短信形式触达用户。本文将带你了解阿里云通信
  18. 《新学期,新FLAG》——如梦初醒
  19. 不同的工具包对Voxel-based morphometry (VBM)计算结果的影响
  20. 通过 Nginx 代理转发配置实现跨域(API 代理转发)

热门文章

  1. linux系统课程ubuntu视频教程
  2. 51单片机学习笔记(8)——74HC573锁存器
  3. 计算机编程abs是什么意思,VB编程中的“Abs”是什么意思?
  4. java游戏猿人时代_猿人时代_JAVA游戏免费版下载_7723手机游戏[www.7723.cn]
  5. 安装arcgis api for python步骤、以及注意事项
  6. jw实验二:配置VLAN Trunks
  7. 静校正问题及其深度学习方法
  8. 解决Linux无法读写U盘中的NTFS问题
  9. vue的介绍-基本语法
  10. 周杰伦新专辑预售热点传播分析报告概览