源码地址:GitHub
思维导图如下:
本文目录如下:
一、数据结构
按照视点的不同,我们把数据结构分为逻辑结构和物理结构
- 逻辑结构
- 集合结构
- 线性结构
- 树形结构
- 图形结构
- 物理结构
- 顺序存储结构
- 链式存储结构
二、算法
- 算法的特性
- 输入输出
- 有穷性
- 确定性
- 可行性
- 算法的设计要求
- 正确性
- 可读性
- 健壮性
- 时间效率高和存储量低
- 算法复杂度
- 时间复杂度
- 大O记法
- 具体用法可以自行百度
- 常见的时间复杂度有O(1)、O(n)、O(n^2)、O(logn)、O(nlogn)、O(n^3)、O(2^n)
- 大O记法
- 空间复杂度
- 我们写代码的时候也可以用空间换取时间,但是通常我们讲的复杂度就是时间复杂度
- 时间复杂度
三、线性结构
1、线性表
零个或多个数据元素的有限序列
- 线性表
- 顺序存储结构
- 链式存储结构
- 单链表
- 静态链表
- 循环链表
- 双向链表
下边代码详细介绍了线性表的各个结构,源码在GitHub
#pragma mark - -----顺序存储结构-----
/**
* 优点:可以快速的读取任何元素
* 缺点:1、插入和删除需要移动大量元素
* 2、长度变化较大时,难以确定存储空间的容量
* 3、造成存储空间的“碎片”
*/
//结构代码
typedef struct{
ElementType data[MAXSIZE];
int length;
}SqList;
//获取元素
Status getElement(SqList *l, int i, ElementType *e){
if(l->length == 0 || i<0 || i>(*l).length){
return ERROR;
}
*e = l->data[i];
return OK;
}
//插入元素
Status listInsert(SqList *l, int i, ElementType *e){
if(l->length == MAXSIZE){
return ERROR;
}
if(i<0 || i>l->length-1){
return ERROR;
}
//将i之后的元素都往后移1位
for(int j=l->length; j>i; j--){
l->data[j] = l->data[j-1];
}
l->data[i] = *e;
l->length ++;
return OK;
}
//删除操作
Status listDelete(SqList *l, int i, ElementType *e){
if(l->length == 0){
return ERROR;
}
if(i<1 || i>l->length-1){
return ERROR;
}
//i之后的元素往前移
for(int j=i; j<l->length-2; j++){
l->data[j] = l->data[j+1];
}
l->length --;
return OK;
}
#pragma mark - -----链式存储结构-----
#pragma mark 单链表 single
/**
* 与顺序存储结构对比
* 优点:1、不需要分配存储空间,元素个数不受限制
* 2、找对对应位置后,插入删除O(1)
* 缺点:1、查找不方便O(n)
*/
//单链表结构代码 这里声明结构体跟给结构体取别名分开来写,方便大家理解
struct Node{
ElementType data;
struct Node *next;
};
typedef struct Node LinkList;
//单链表获取元素
Status getElementSingle(LinkList *l, int i, ElementType *e){
LinkList *p = l->next;
int j = 0;
while (p && j<i) {
//一般来说 p->用来指向p中的结构体,但是也跟声明方式有关系
p = p->next;
j++;
}
//循环到最后p都不存在了,也没找到第i个元素,这种情况就是i 比j大,但是只有循环完了才能知道i的比链表的长度大
if(!p){
return ERROR;
}
*e = p->data;
return OK;
}
//单链表插入元素
Status listInsertSingle(LinkList *l, int i, ElementType *e){
LinkList *p = l->next;
int j = 0;
while(p && j<i){
p = p->next;
j++;
}
if(!p){
return ERROR;
}
LinkList *s = (LinkList *)malloc(sizeof(struct Node));
s->data = *e;
s->next = p->next;
p->next = s;
return OK;
}
//单链表删除元素
Status listDeleteSingle(LinkList *l, int i, ElementType *e){
LinkList *p = l->next;
int j=0;
while(p && j<i){
p = p->next;
j++;
}
if(!p){
return ERROR;
}
p->next = p->next->next;
*e = p->next->data;
free(p->next);
return OK;
}
//单链表的整表创建--头插法
void createListHead(LinkList *l, int n){
LinkList *p;
//创建头结点
l = (LinkList *)malloc(sizeof(struct Node));
l->next = NULL;
for(int i=0; i<n; i++){
p = (LinkList *)malloc(sizeof(struct Node));
p->data = rand()%100+1;
p->next = l->next;
l->next = p;
}
}
//单链表的整表创建--尾插法
void createListTail(LinkList *l, int n){
LinkList *p;
l = (LinkList *)malloc(sizeof(struct Node));
for(int i=0; i<n; i++){
p = (LinkList *)malloc(sizeof(struct Node));
p->data = rand()%100+1;
l->next = p;
l = p;
}
}
//单链表的整表删除
Status clearList(LinkList *l){
LinkList *p, *q;
p = l->next;
while (p) {
q = p->next;
free(p);
p = q;
}
l->next = NULL;
return OK;
}
#pragma mark 静态链表 static
/**
* 用数组描述的链表叫静态链表
* 让数组的元素由data、cur两个数据域组成,data存放数据,cur存放该元素后继
* 元素的下标,相当于单链表中的next指针,我们把cur叫做游标。
*
* 优点:插入删除时只需要修改游标,不需要移动元素
* 缺点:1、没有解决连续存储分配带来的表长难以确定的问题
* 2、失去了顺序存储结构随机存取的特性(并没有理解。。。)
* tips:
* 1、把未被使用的数组元素称为备用链表。
* 2、数组的第一个跟最后一个元素做特殊处理,不存数据。第0个元素的cur存放备用链表第1个结点
* 的下标,数组的最后一个元素存放第一个有数值元素的下标,
*/
//静态链表结构代码
typedef struct {
ElementType data;
int cur;
}StaticLinkList[MAXSIZE];//因为静态链表本质还是数组,所以要声明为数组,
//由于是数组,所以我们来模仿malloc数组的操作,返回当前备用链表的第一个游标
int malloc_SLL(StaticLinkList space){
int i = space[0].cur; //取出备用链表的第一个元素
if(space[0].cur){
space[0].cur = space[i].cur;//备用链表的第一个要被使用了,把第二个拿来做备用
}
return i;
}
//free操作,把i回收到备用链表
void free_SLL(StaticLinkList space, int i){
space[i].cur = space[0].cur;
space[0].cur = i;
}
//静态链表获取元素,都是数组了,直接用下标拿就可以了
//静态链表插入元素
Status staticLinkListInsert(StaticLinkList l, int i, ElementType e){
if(i<1 || i>MAXSIZE){
return ERROR;
}
int j = malloc_SLL(l);
l[j].data = e;
int m = MAXSIZE-1;//最后一个元素的下标,cur存放的就是第一个元素的下标
for(int k=1; k<i; k++){
m = l[m].cur;
}
l[j].cur = l[m].cur;
l[m].cur = j;
return OK;
}
//静态链表删除元素
Status staticLinkListDelete(StaticLinkList l,int i){
if(i<0 || i>MAXSIZE){
return ERROR;
}
int j = MAXSIZE-1;
for(int k=0; k<i-1; k++){
j = l[j].cur;
}
l[j].cur = l[l[j].cur].cur;
free_SLL(l, l[j].cur);
return OK;
}
//静态链表长度
int StaticLinkListLength(StaticLinkList l){
int i=0;
int j = l[MAXSIZE-1].cur;
while (j) {
j = l[j].cur;
i++;
}
return i;
}
#pragma mark 循环链表 cycle
/**
* 循环链表其实就是让最后一个节点的指针指向头节点。
* 如果需要合并ab两个循环链表,只需要让a的尾指针指向b的第一个结点,让b的尾指针指向a的头结点。
* 然后把b的头节点free掉即可。
* 如果不懂可以去google一下网上的图片。
*/
#pragma mark 双向链表 double
/**
* 双向链表:在单链表的每个节点中,再设置一个指向其前驱结点的指针域,
* 可以google图片,比较通俗易懂。
*/
①、栈
限定仅在表尾进行插入和删除操作的线性表
#pragma mark - -----栈的顺序存储结构-----
#pragma mark 栈
//进栈操作
Status push(Stack *s, ElementType e){
if(s->top == MAXSIZE-1){//栈满
return ERROR;
}
s->top ++;
s->data[s->top] = e;
return OK;
}
//出栈操作
Status pop(Stack *s, ElementType *e){
if(s->top == -1){//空栈
return ERROR;
}
*e = s->data[s->top];
s->top--;
return OK;
}
#pragma mark 两栈共享空间
// 空间结构
struct doubleStack{
ElementType data[MAXSIZE];
int top1;//栈1 栈顶指针
int top2;//栈2 栈顶指针
};
typedef struct doubleStack DoubleStack;
//插入元素
Status pushDouble(DoubleStack *d, ElementType e, int stackNumber){
if(d->top1 + 1 == d->top2){//栈满了
return ERROR;
}
if(stackNumber == 1){
d->data[d->top1+1] = e;
}else if(stackNumber == 2){
d->data[d->top2-1] = e;
}
return OK;
}
//删除元素
Status popDouble(DoubleStack *d, ElementType *e, int stackNumber){
if(stackNumber == 1){
if(d->top1 == -1){
return ERROR;
}
*e = d->data[d->top1];
d->top1--;
}else if(stackNumber == 2){
if(d->top2 == MAXSIZE){
return ERROR;
}
*e = d->data[d->top2];
d->top2++;
}
return OK;
}
#pragma mark - -----栈的链式存储结构-----
/**
* 其实栈的链式存储结构跟单链表非常非非常类似了
* 其实我都超级不想写了。一模一样的东西。
*/
#pragma mark - -----栈的应用-----
#pragma mark 递归-斐波那契数列
/**
* 当前项的值等于前面两项的和
*/
//求第n项的值
int fbi(int n){
if(n == 1){
return 1;
}else if(n == 2){
return 1;
}else{
return fbi(n-1) + fbi(n-2);
}
}
#pragma mark 后缀表达式
/**
* 感觉是反人类的表达式哈哈,反正看规则能看懂,自己写脑壳痛。
*
* 中缀表达式转后缀表达式规则:
* 从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;
* 若是符号,则判断其与栈顶符号的优先级,是右括号或优先级不高于栈顶符号则栈顶元素依次出栈并输出,
* 并将当前符号进栈,一直到最终输出后缀表达式为止。
*/
②、队列
只允许在一端进行插入操作,在另一端进行删除操作的线性表
#pragma mark - -----队列的顺序存储结构-----
/**
* 队列的顺序存储结构跟线性表的顺序存储结构一毛一样,不再赘述。
* 队列就跟排队一样,队头出队,那么剩下的元素都要往前移动,所以缺点明显,就引入的循环队列
*/
#pragma mark 循环队列的顺序存储结构
/**
* 空队列时,front=rear,但是队列满时front也=rear,那怎么判断队列是空还是满呢?
* 方法一:设置flag,当front=rear,且flag=0,队列为空;当front==rear,且flag=1,队列满。
* 方法二:front=rear队列空,当队列满时我们保留一个元素空间,这样就好判断了。
* 下面介绍的就是第二种方法。
*/
//初始化一个空队列
Status initQueue(Queue *q){
q->front = 0;
q->rear = 0;
return OK;
}
/**
* 循环队列长度 这个可以推倒,一种情况是rear>front len=rear-front
* 另一种情况是rear<front,一部分长度是rear另一部分长度是maxsize-front ==> rear+maxsize-front
* 这样可以推倒出通用长度公式就是下边的部分。
*/
int queueLength(Queue q){
return (q.rear - q.front+MAXSIZE)%MAXSIZE;
}
//入队列操作
Status inQueue(Queue *q, ElementType e){
if(queueLength(*q) == MAXSIZE -1){
return ERROR;
}
q->data[q->rear] = e;
q->rear = (q->rear+1)%MAXSIZE;
return OK;
}
//出队列操作
Status outQueue(Queue *q, ElementType *e){
if(q->rear == q->front){
return ERROR;
}
*e = q->data[q->front];
q->front = (q->front+1)%MAXSIZE;
return OK;
}
#pragma mark - -----队列的链式存储结构-----
/**
* 完全参考单链表的链式存储就可以了。很简单,插入的时候插尾部,取出的时候取头部 LIFO last in first out.
*/
2、串
由零个或多个字符组成的有限序列,又叫字符串
/**
* 串其实就是字符串,这里感觉讨论串没什么意义,直接就开始整模式匹配算法把。
* 返回子串在主串中的位置,若不存在,返回0
*/
#define ERROR 0
#define OK 1
typedef int Status;
#pragma mark - 朴素的模式匹配算法
/**
* 第一种就是大家都能想到的双层for循环
* 这里给大家写while里边指针回溯的方法,其实本质也就是两层循环,
* 那我费半天话干什么skr
*/
int indexWithBigString(String shortStr, String longStr){
int i=0, j=0;
while(i<shortStr.len && j<longStr.len){
if(shortStr.data[i] == longStr.data[j]){
i++;
j++;
}else{
j=j-i+1;
i=0;
}
if(i == shortStr.len){
return j-i;
}
}
return 0;
}
#pragma mark - KMP模式匹配算法
/**
* KMP模式匹配算法的重点就是next[]的计算,
* while 的条件里要有一个 i==0 的情况,这样j才能一直+
*/
int KMPIndex(String shortStr, String longStr){
int next[shortStr.len];
calNext(shortStr.data, next);
int i=0, j=0;
while(i<shortStr.len && j<longStr.len){
if(i==0 || shortStr.data[i]==longStr.data[j]){
j++;
i++;
}else{
i = next[i];
}
if(i==shortStr.len){
return j-i;
}
}
return 0;
}
//这个推荐大家看阮一峰的博客 http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
void calNext(const char p[],int next[]){
int len = (int)strlen(p);//匹配字符串的长度
next[0] = 0;
//i:模版字符串下标;j:最大前后缀长度
for(int i=1,j=0; i<len; i++){
while(j>0 && p[i] != p[j]){
j = next[j-1];
}
if(p[i] == p[j]){
j++;
}
next[i] = j-1;
}
for(int k=0; k<len; k++){
printf("next[%d] === %d \n",k,next[k]);
}
}
#pragma mark - KMP模式匹配算法改进
/**
* 这个优化主要是针对next[]的优化
* 如果子串是 aaab 主串是 aaaxdfdlfjdlfj 就会发现next[]的小小的缺点了。
*
*/
int KMPIndexOptimize(String shortStr, String longStr){
int next[shortStr.len];
calNextOptimize(shortStr.data, next);
int i=0, j=0;
while(j<longStr.len){
if(i==0 || shortStr.data[i]==longStr.data[j]){
j++;
i++;
}else{
i = next[i];
}
if(i==shortStr.len){
return j-i;
}
}
return 0;
}
void calNextOptimize(const char p[],int next[]){
int len = (int)strlen(p);//匹配字符串的长度
next[0] = 0;
//i:模版字符串下标;j:最大前后缀长度
for(int i=1,j=0; i<len; i++){
while(j>0 && p[i] != p[j]){
j = next[j-1];
}
if(p[i] == p[j]){
j++;
}
if(i>0 && p[i] == p[i-1]){
next[i] = next[next[i]];
}else{
next[i] = j-1;
}
}
printf("优化后的数组:\n");
for(int k=0; k<len; k++){
printf("next[%d] === %d \n",k,next[k]);
}
}
四、树形结构
1、树
2、二叉树
/**
* 特殊的二叉树:
*
* 1、斜树 又包括左斜树,右斜树
* 2、满二叉树 每个节点都存在左右子树,而且所有的叶子都要在同一层上
* 3、完全二叉树 对一颗有n个结点的二叉树按层序编号,如果编号为i的结点与同样深度的满二叉树中编号为i的结点在二叉树中的位置完全相同,
*/
/**
* 二叉树的性质:
*
* 1、在二叉树的第i层上之多有2^i-1 个结点
* 2、深度为k的二叉树之多有2^k-1个结点
* 3、对于任何一个二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1
* 4、具有n个结点的完全二叉树的深度为|log2N| +1 (|x| 表示不大于x的最大整数)
* 5、第五个贼繁琐,懒得抄了。
*/
#pragma mark - -----二叉树的顺序存储结构-----
//按照序号存储,如果是满二叉树那么比较好,如果不是满二叉树就比较浪费内存,所以直接介绍链式存储结构
#pragma mark - -----二叉树的链式存储结构-----
#define ERROR 0
#define OK 1
typedef int ElementType;
typedef int Status;
#pragma mark 二叉链表
/**
* 二叉链表:二叉树每个节点最多有两个孩子,所以为它涉及一个数据域和两个指针域,这样的链表就是二叉链表。
*/
//结构定义
struct node{
ElementType data;
struct node *lchild, *rchild;
};
typedef struct node BinaryTree;
#pragma mark 遍历二叉树
/**
* 二叉树的遍历方式有很多,如果我们限制了从左到右的习惯,那么主要分为4种,前序、中序、后序、层序。
*/
//前序遍历 先根节点,次左子树,后右子树
void preorderTraversal(BinaryTree *t){
if(t == NULL){
return;
}
printf("%c",t->data);
preorderTraversal(t->lchild);
preorderTraversal(t->rchild);
}
//中序遍历 先左子树,次根节点,后右子树
void middleorderErgodic(BinaryTree *t){
if(t == NULL){
return;
}
preorderTraversal(t->lchild);
printf("%c",t->data);
preorderTraversal(t->rchild);
}
//后序遍历 先左子树,次右子树,后根节点
void postrderTraversal(BinaryTree *t){
if(t == NULL){
return;
}
preorderTraversal(t->lchild);
preorderTraversal(t->rchild);
printf("%c",t->data);
}
//层序遍历 从第一层开始,从上而下每一层遍历,同一层中从左到右顺序遍历
//推倒遍历结果
/*
已知前序为ABCDEF 中序为CBAEDF 求后序
答:你们自己百度吧,简单的一逼
*/
#pragma mark - -----二叉树的建立-----
/**
* A
* | |
* B C
* |
* D
* 现有二叉树如上图所示,我们先把二叉树补全,保证每个结点都有左右孩子
* A
* | |
* B C
* | | | |
* # D # #
* | |
* # #
* 这样二叉树的前序遍历结果为:AB#D##C##
*/
void createTree(BinaryTree *t){
ElementType ch;
scanf("%d", &ch);
if(ch == '#'){
t = NULL;
}else{
t = (BinaryTree *)malloc(sizeof(struct node));
if(! t){
exit(ERROR);
}
t->data = ch;
createTree(t->lchild);
createTree(t->rchild);
}
}
#pragma mark 线索二叉树
/**
* 二叉树按照某种次序遍历每个结点都有前驱后继指针,这样的二叉树就是线索二叉树
*
* 线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索,所以线索化的过程就是遍历的过程
* 感觉线索化的过程跟其他的思想简直一样,这里只实现结构。
*/
//线索二叉树的结构实现
enum child{
lrChild, //lrChild=0 表示指向左右孩子的指针
thread //thread=1 表示指向前驱或后继的线索
};
typedef enum child pointerTag;
struct thrNode{
ElementType data;
struct thrNode *lchild, *rchild;
pointerTag lTag;
pointerTag rTag;
};
typedef struct thrNode thrTree;
#pragma mark - -----树、森林与二叉树的转换-----
/**
* 树、森林与二叉树的转换这里不做赘述了。需要的时候查资料,按照步骤转换即可。
*/
#pragma mark 赫夫曼树及其应用
/**
* 带权路径长度WPL最小的二叉树称做赫夫曼树,具体步骤:
* 1、根据给定的n个权值{W1,W2,...,Wn}构成n棵二叉树的集合F={T1,T2,...Tn},其中每棵二叉树Ti中只有一个带权为Wi的根节点,其左右子树均为空。
* 2、在F中选取两棵根节点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根节点的权值为其左右子树上根结点的权值之和。
* 3、在F中删除这两棵树,同时将新得到的二叉树加入F中。
* 4、重复2和3步骤,直到F只含一棵树位置。这棵树便是赫夫曼树。
*/
/**
* 赫夫曼编码: 把文字出现的频率当做权,然后用赫夫曼树来确定其表达的方式,形成了赫夫曼编码。
* 这个概念可以具体google一下,只要懂了赫夫曼树,赫夫曼编码就很容易懂了。
*/
3、红黑树
五、图形结构
#pragma mark - -----图的存储结构-----
#pragma mark 邻接矩阵
/**
* 注意A[0][3]代表第0行,第3列
* 二维数组表示结点之间的关系
*/
#pragma mark 邻接表
/**
* 一个顶点用一个线性表表示,每个数据元素指向第一个邻接点的信息,以此类推,当然了如果有权值,
* 还可以存放对应的权值
*/
#pragma mark 十字链表
/**
* 把邻接表、逆邻接表结合起来就是十字链表,每个顶点有一个线性表
*/
#pragma mark 邻接多重表
/**
* 乱,不想看。
*/
#pragma mark 边集数组
/**
* 乱上加乱
*/
#pragma mark - -----图的遍历-----
#pragma mark 深度优先遍历
/**
* DFS depth_frist_search
*/
#pragma mark 广度优先遍历
/**
* BFS breadtch_first_search
*/
#pragma mark - -----最小生成树-----
//普利姆算法