第一章:
这里是引用
数据结构绪论
一.什么是程序
程序设计=数据结构 + 算法
就是数据元素相互之间存在的一种或多种特定关系的集合
数据结构:逻辑结构与物理结构
二.逻辑结构&物理结构的区别用法
(一):逻辑结构(面向问题的可具体分为以下四种关系):是指数据对象中数据元素之间的相互关系
A:集合结构
数据元素除了同属于一个集合外,它们之间没有其他关系
B:线性结构
数据元素之间是一对一关系
C:树形结构
数据元素之间呈现一对多关系
D:图形结构
数据元素是多对多关系
(二):物理结构:是指数据的逻辑结构在计算机中的存储形式
A:什么是数据
数据是数据元素的集和
B:什么是存储器
存储器主要是针对内存而言的,向硬盘,软盘,光盘等外部存储器的数据组织–通常用文件结构来描述
三.顺序存储&链式存储的区别用法
数据元素的存储结构形式有两种:顺序存储和链式存储
(一)顺序存储结构
是把数据源存放在地址连续的存储单元里
其数据间的逻辑关系和物理关系是一致的
(二)链式存储结构
是把数据元素存放在任意的存储单元里
这组存储单元可以是连续的
也可以是不连续的
第二章:算法
一.定义
算法是解决特定问题求解步骤的描述
在计算机中**表现**为指令的有限序列
并且每条指令**表示**一个或多个操作
简而言之,**算法是描述解决问题的方法**
二.特性
输入、输出、有穷性、确定性和可行性
==好的算法==:正确性、可读性、高效性和低存储量的特征
三.算法时间效率度量
(1)可以忽略加法常量
O(2n + 3) = O(2n)
(2)与最高次项相乘的常量可忽略
O(2*n^2) = O(n^2)
(3)最高次项的指数大的,函数随着n的增长,结果也会变得增长得更快
O(n^3) > O(n^2)
(4)判断一个算法的(时间)效率时,函数中常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数
O(2n^2) = O(n^2+3n+1)
O(n^3) > O(n^2)
四.时间复杂度&空间复杂度的区别用法
(1)时间复杂度:大O阶推导方法
-
定义
- 即基本操作执行次数
-
时间复杂度的计算
- 用常数1取代运行时间中的所有加法常数
- 在修改后的运行次数函数中,只保留最高阶项
- 如果最高阶项存在且不是1,则去除与这个项相乘的常数,即O(2*n^2)=O(n
- ^2),得到的结果就是大O阶
-
分类
* 线性阶
int i,j;
for ( i = 0; i < n; ++i){
for(j = i; j < n; ++j){
/*时间复杂度为 O(1) 的程序步骤序列 */
}
* 平方阶
int i . j , n =100;
for ( i =0; i < n; j++)
{
for(j=i; j <n; j++)
{
printf("I love fishC.com\n");
}
}
n(n+1)/2=n^2/2+n/2
O(n^2)
* 对数阶
int i=1,n=100;
while(i < n)
{
i=i*2;
}
2^x=n-->x=log(2)n
O(log2 n)
- 分析
对于外循环,其时间复杂度为 O(n);
对于内循环环,当 i=0 时,内循环执行了 n 次,当 i=1 时,执行了 n-1 次,······当 i=n-1 时,执行了 1 次。
因此内循环总的执行次数为
n + (n-1) + (n-2) + \cdots + 1 = \frac{n(n+1)}{2} = \frac{n^2}{2} + \frac{n}{2}
根据大 O 阶推导方法,最终上述代码的时间复杂度为 :
O(n^2)
- 常见的时间复杂度
(2)空间复杂度
- 空间复杂度的计算
利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计
一个算法所需的存储空间用f(n)表示
空间复杂度的计算公式记作:S(n)=O(f(n))
其中n为问题的规模
S(n)表示空间复杂度
- 计算方法:
(1)忽略常数,用O(1)表示
举例1:
a = 0
b = 0
prinft(a,b)
它的空间复杂度O(n)=O(1);
(2)递归算法的空间复杂度=递归深度N*每次递归所要的辅助空间
举例2:
def fun(n): k = 10
if n == k:
return n
else:
return fun(++n)
递归实现,调用fun函数,每次都创建1个变量k。调用n次,空间复杂度O(n*1)=O(n)。
(3)对于单线程来说,递归有运行时堆栈,求的是递归最深的那一次压栈所耗费的空间的个数
因为递归最深的那一次所耗费的空间足以容纳它所有递归过程
举例3:
temp=0;
for(i=0;i<n;i++):
temp = i
变量的内存分配发生在定义的时候,因为temp的定义是循环里边,所以是nO(1)
如果temp定义在循环外边,所以是1O(1)
(4)
- 空间复杂度 O(1)
如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
变量 i、j、m 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)
- 空间复杂度 O(n)
int[] m = new int[n]
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)
3.需存储空间包括以下两部分
(1)固定部分
这部分属于静态空间
这部分空间的大小与输入/输出的数据的个数多少、数值无关
主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间
(2)可变空间
这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等
这部分的空间大小与算法有关
(3)常用的算法的时间复杂度和空间复杂度
第三章 线性表(List)
一.线性表的顺序存储结构
(1)定义
- 传统
是用一段地址连续的存储单元依次存储线性表的==数据元==(相同数据类型元素)
- 线性表的抽象数据类型定义(就是把数据类型和相关操作捆绑在一起)OPTION
* InitLIst(*L): 初始化操作,建立一个空的线性表L。
* ListEmpty(L): 判断线性表是与否为空表,若线性表为空,返回true,否则返回false
* ClearList(*L): 将线性表清空。
* GetElem(L,i,*e):将线性表L中的第i个位置元素值返回给e。
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
//Status是函数类型,气质是函数结果状态代码
//初始条件L顺序线性表L已存在,1 <= i <= ListLength(L);
//操作结果:用e返回L中第i个数据元素的值
Status GetElem(SqList L,int i,ElemType *e)
{
if(L.length == 0|| i>1|| i>L.length)
{
return ERROR;
}
*e = L.data(i-1);
return OK;
}
* LocateElem(L,e):在线性表L中查找与给定值e相等的元素,如果查找成功,返回钙元素在表中序号,否则返回0.
* ListInsert(*L , i , e): 在线性表L中第i个位置插入新元素e。
Status ListInsert(SqList *L,int i,ElemType e)
{
int k;
if(L->length == MAXSIZE) //顺序线性表已经满了
{
return ERROR;
}
if(i<1 || i>L->length +1) //当i不在范围内时
{
return ERROR;
}
if(i <= L->length) //若插入数据位置不在表尾
{
//将要插入位置后的所有数据元素向后移动一位
for( k=L->length-1 ;k>= i-1; k--)
{
L->data[k+1] = L->data[k];
}
}//插入新元素
L->data[i-1] = e;
L->length++;
return OK;
}
* ListDelete(*L,i,*e):返回线性表L的元素个数。
//出事条件,顺序线性表L已存在,1 <= i <= ListLength(L)
//操作结果,删除L的第i个数据元素,并用e返回其值,L的长度-1
Status ListDelete (SqList *L,int i, ElemType *e)
{
int k;
if ( L ->length == 0)
{
return ERROR;
}
if(i<1 || i>L->length)
{
return ERROR;
}
if(i < L->length)
{
for(k=i ; k < L->length ; k++)
{
L->data[k-1] =L->data[k];
}
}
L->length--;
return Ok;
}
(2)顺序储存示意图如下所示:
- 存储结构
- 代码结构
#define MAXSIZE 20
typedef int ElemType;
typedef struct
{
Elemtype data[MAXSIZE];
int length;
}SqList;
储存空间的起始位置,数组data
线性表的最大存储容量:数组的长度MaxSize。
线性表的当前长度:length。
(3)编号地址
- 存储器中的每个存储单元都有自己的编号,这个编号称为地址
(4)储存位置公式
每个数据元素,不管它是整型,实型还是字符型,它都是需要占用一定的存储单元空间的
假设占用的是 c 个存储单元,那么对于线性表的第 i 个数据元素 的存储位置都可以由 推导算出:
(5)存取操作时间性能
- 通过该公式,就可以随时算出线性表中任意位置的地址
不管是第一个还是最后一个,==都是相同的时间
也即对于线性表每个位置的存入或者取出数据
对于计算机来说都是相等的时间==,也就是一个常数时间
因此,线性表的存取操作时间性能为O(1)
(6)随机存储结构
- 我们通常将存取操作具备常数性能O(1)的存储结构称为随机存储结构
(7)时间复杂度
- 对于存取操作
线性表的顺序存储结构,对于存取操作,其时间复杂度为 O(1),因为元素位置可以直接计算得到 - 对于插入和删除操作
插入算法思路:
- 位置不合理,抛出异常
- 线性表长度大于等于数组长度,则抛出异常或动态增加数组容量
- 从最后一个元素开始向前遍历到第i个位置,分别将他们都向后移动一个位置
- 线性表长+1
对于插入和删除操作,其时间复杂度为O(n)
因为插入或删除后,需要移动其余元素
(8)使用场景
- 优点
- 无需为表示表中元素之间的逻辑关系而增加额外的存储空间。
- 可以快速地存取表中任意位置的元素。 - 缺点
- 插入和删除操作需要移动大量元素
- 当线性表长度比较大时,难以确定存储空间的容量
- 相邻元素的存储位置也具有邻居关系,他们在内存中的位置是紧挨这的,中间没有间隙,淡然就无法快速插入和删除。
二.线性表的链式存储结构
(1)什么是链表
- 每个元素多用一个位置来存放指向向下一个元素的位置的指针
- 一个或多个结点 组合而成的数据结构称为链表(线性表由于可以通过简单的地址公式查找到指定元素,而链表没有固定的公式,所以只能遍历)
(2)节点
-
数据域与指针域组成数据元素称为存储映像(结点)
-
数据域
存储真实数据元素 -
指针域
存储下一个结点的地址(指针)
(3)头指针&头结点
-
头结点的数据域可以不存储任何信息,其指针域存储指向第一个结点的指针(即指向头指针)
-
头指针
一般把链表中的第一个结点称为 头指针,其存储链表的第一个数据元素 -
头结点
为了能更加方便地对链表进行操作,会在单链表的第一个结点(即头指针)前附设一个结点,称为 头结点,其数据域一般无意义;最后一个结点指针为NULL。
(4)单链表
定义
在线性表的顺序存储结构(即数组)中,其任意一个元素的存储位置可以通过计算得到,因此其数据读取的平均时间复杂度为 O(n)
结构指针来描述单链表
TYPEDEF STRUCT NODE
{
ElemType data;
struct Node* Next;
}Node;
typedef struct Node* LinkList;
p->data = ai
p->next->data = ai+1
单链表的时间复杂度
- 对于存取操作
- 读取代码 -
Status GetElem( LinkList L,int i, ElemType *e)
{
int j;
LinkList p;
p = L->next;
j = 1;
while( p && j<i)
{
p = p->next;
++j;
}
if( !p || j>i)
{
return ERROR;
}
*e = p->data;
return OK;
}
而对于单链表结构,假设需要获取第 i 个元素,则必须从第一个结点开始依次进行遍历,直到达到第 i 个结点。因此,对于单链表结构而言,其数据元素读取的时间复杂度为 O(n)。
- 对于插入和删除操作
而对单链表结构来说,对其任意一个位置进行增删操作,其时间复杂度为 O(n)。对头指针的增删操作其时间复杂度为 O(1)。
插入与删除
- 插入语句
Status ListInsert ( LinkList * L,int i, ElemType e)
{
int j;
LinkList p,s;
p = *L;
j = 1;
while( p && j<i )
{
p = p->next;
j++;
}
if( !p || j>i )
{
return ERROR;
}
s = (LinkList)malloc(sizeof(Node));
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
//不能弄反
- 删除语句
Status ListDelete(LinkList *L,int i, ElemType *e)
{
int j;
LinkList p,q;
p = *L;
j = 1;
while(p->next && j<i)
{
p = p ->next;
++j;
}
if( !(p->next) || j >i )
{
return ERROR;
}
q = p ->next;
p ->next = q->next;
*e = q->next;
free(q);
return OK;
}
单链表的建立
头插法
- 从一个空表开始,生成新结点,读取数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头上,直到结束。
//就是将新加进的元素放在表头后的第一个位置
//fishc
//head -> c -> h -> s -> i -> f
void CrateListHead( linkList *L, int n)
{
LinkList p;
int i;
srand(time(0));
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL;
for( i=0 ; i<n ; i++)
{
p = (LinkList)malloc(sizeof(Node));
p->next = (*L)->next;
(*L)->next = p ;
}
}
尾插法
- 把新结点都插入到最后结点之后
void CrateListTail(LinkList *L, int n)
{
LinkList p, r;
int i;
srand(time(0));
*L = (LinkList)malloc(sozeof(Node));
r = *L;
for( i=0 ; i<n ; i++);
{
p = (Node *)malloc (sizeof(Node));
p->data = rand()%100+1;
t->nexr = p;
r = p;//反复的向下移动
}
r->next = NULL;
}
静态链表的插入操作
- 动态链表,结点的申请和释放分别借用C语言的malloc()和free()两个函数来实现
- 静态链表,操作的数组,需要自己实现函数,才可以做到插入和删除(静态链表其实是为了给没有指针的编程语言设计的一种实现单链表功能的方法)
- 游标是指向下一结点的下标
- 首先是获取空闲分量的下标
int Malloc_SLL(StaticLinkList space)
{
int i = space[0].cur;
if( space[0].cur )
space[0].cur = space[i].cur;
return i;
}
- 插入函数(在A后面插入B)
Status ListInsert( StaticLinkList L, int i, ElemType e)
{
int j , k , l;// i 是第几个元素插入
k = MAX_SIZE -1;//数组最后一个元素
if( i<1 || i >ListLength(L) + 1)
{
return ERROR;
}
j = Malloc_SLL(L);//获取第一个空闲的下标
if(j)
{
L[j].data = e;
for( l = 1; l <=i-1 ; l++)
{
k = L[k].cur;//最后一个游标数十指向第一个游标数的k=1
}
L[j],cur = L[k].cur; //5,cur=1.cur
L[k].cur = j; //1.cur
return OK;
}
return ERROR;
}
- 删除语句(删除C)
//删除在L中的第i个数据元素
Status ListDelete( StaticLinkList L, int i)
{
int j, k;
if( i<1 || i>ListLength(L))
{
return ERROR;
}
k = MAX_SIZE -1;
for( j=1 ; j <=i ; j++)
{
k = L[k].cur;//循环两次 k=1,k=5
}
j = L[k].cur;//j=2
L[k].cur = L[j].cur;
Free_SLL(L,j);
return OK;
}
//将下标位k的空闲结点回收到备用链表
void Free_SLL(StaticList space , int k)
{
space[k].cur = space[0].cur;
space[0].cur = k ;
}
//返回L中数据元素个数
int Listlength(StaticLinkList L)
{
int j =0 ;
int i = L[MAXSIZE-1].cur;
while(i)
{
i = L[i].cur;
j++;
}
return j;
}
(5)线性表和单链表那个好
- 插入连续10个元素,对于顺序存储结构意味着,每次插入都需要移动 n-i 个位置,所以每次都是O(n);
- 二单链表,我们只需执行一次,找到第i位置的指针,此时为O(n),接下来只是简单地通过赋值移动指针而已,时间复杂度都是O(1).
(6)循环链表
定义
- 将单链表中终端结点的指针端由空指针改为指向头节点,就是整个单链表形成一个环,这种头尾相接的单链表成为单循环链表
代码实现
- 初始化部分:ds_init.c
- 插入部分:ds_insert.c
- 删除部分: ds_delete.c
- 返回结点所在位置:ds_search,c
约瑟夫环
循环链表特点
- 用O(1)的时间就可以由链表指针访问到最后一个结点,只需修改指针,无需遍历;二单链表上做这种链接操作,都需要遍历链表,其执行时间是O(n)。
- 两个单链表的连接只需一个头结点
(7)判断单链表是否有环
-
链表的尾结点指向了链表中的某个结点
-
判断方法
(1)使用p、q两个指针,p总是向前走,但q每次都从头开始走,对于每个结点,看p走的步数是否和q一样
(2)使用p、q两个指针,p每次向前走一步,q每次向前走两步,若在某个时候p == q,则存在环。 -
魔术师发牌问题和拉丁方阵
(8)双向链表
- 双向链表结构
typeof struct DualNode
{
ElemType data;
struct DualNode *prior;//前驱结点
struct DualNode *next;//后继结点
} DualNode,*DulinkList;
- 插入代码
s->next = p;
s->prior = p->prior;
p->prior-next = s;
p->prior = s;
- 删除代码
p->prior->next = p ->next;
p->next->prior = p ->prior;
free(p);
- 实践
要求实现用户输入一个数使得26个字母的排列发生变化,例如用户输入+3,输出结果:EFGH…ABC;输入-3,向前移动3位。
第四章:栈与队列
一、什么是栈
- 栈是线性表的实际形式,是一个后进先出的特殊线性表,它要求只在表尾进行删除和插入操作
栈顶
- 允许插入和删除的一端(表尾)
栈底
- 相应的表头
空栈
- 不含任何数据元素的栈(栈顶就是栈底) top = -1;
二、顺序栈&链栈
顺序栈
- 定义栈存储结构
typeof int ElemType
typedef struct
{
ElemType *base;
ElemType *top;
int stackSize;
}sqStack;
//创建一个栈
#define STACK_INIT_SIZE 100
initStack(sqStack *s)
{
s->base = (ElemType *)malloc(STACK_INIT_SIZE * sizeof(ElemType));
if(!s->base)
exit(0);
s->top = s-> base;//最开始,栈顶就是栈底
s->stackSize = STACK_INIT_SIZE;
}
- 出栈
#define SATCKINCREMENT 20
push(sqStack *s, ElemType e)
{
if( s->top - s->base >= s->stackSize)
{
s->base = (ElemType *) realloc(s->base ,(s->stackSize+STACKINCREMENT) + sizeof(ElemType));//增加栈的容量
if( !s->base )
exit(0);
s->top = s->base + s->stackSize;//设置栈顶
s->stackSize = s->stackSize + STACKINCRMENT;//设置栈的最大容量
}
*(s->top) = e;
s->top++;
}
- 入栈
pop(sqStack *s , ElemType *e)
{
if( s->top == s->base)//栈已空空是也
return;
*e = * -- (s->top);
}
- 清空栈(就是将站中的元素全部作废,但栈本身物理空间并不发生改变)
ClearStack( sqStack *s){
s->top = s->base;
}
- 销毁一个栈(要释放掉栈所占据的物理内存空间)
DestroyStack(sqStack *s){
int i , len ;
len = s->stackSize;
for( i=0; i<len ;i++){
free( s->base );
s->base++;
}
s->base = s->top =NULL;
s-> stackSize = 0;
}
- 计算栈的当前容量
int StackLen(sqStack s)
{
return ( s,top -s.bse);//得到的是几个元素
}
- 实例分析3进制转换为10进制
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#define STACK_INIT_SIZE 20
#define STACKINCREMENT 10
typedef char ElemType;
typedef struct
{
ElemType *base;
ElemType *top;
int stackSize;
}sqStack;
void InitStack(sqStack *s)
{
s->base = (ElemType *)malloc(STACK_INIT_SIZE * sizeof(ElemType));
if( !s->base )
{
exit(0);
}
s->top = s->base;
s->stackSize = STACK_INIT_SIZE;
}
void Push(sqStack *s, ElemType e)
{
if( s->top - s->base >= s->stackSize )
{
s->base = (ElemType *)realloc(s->base, (s->stackSize + STACKINCREMENT) * sizeof(ElemType));
if( !s->base )
{
exit(0);
}
}
*(s->top) = e;
s->top++;
}
void Pop(sqStack *s, ElemType *e)
{
if( s->top == s->base )
{
return;
}
*e = *--(s->top);
}
int StackLen(sqStack s)
{
return (s.top - s.base);
}
int main()
{
ElemType c;
sqStack s;
int len, i, sum = 0;
InitStack(&s);
printf("请输入二进制数,输入#符号表示结束!\n");
scanf("%c", &c);
while( c != '#' )
{
Push(&s, c);
scanf("%c", &c);
}
getchar(); // 把'\n'从缓冲区去掉
len = StackLen(s);
printf("栈的当前容量是: %d\n", len);
for( i=0; i < len; i++ )
{
Pop(&s, &c);
sum = sum + (c-48) * pow(2, i);
}
printf("转化为十进制数是: %d\n", sum);
return 0;
}
了利用栈将二进制转换为十进制
链栈
- 栈因为只是栈顶来做插入和删除操作,所以比较好的方法就是讲栈顶放在单链表的头部,栈顶指针和单链表的头指针合二为一。
- 链栈结构
teypedef struct StackNode
{
ElemType data;
struct StackNode *next;//存放栈的数据
} StackNode,*LinkStackPtr;
teypedef struct LinkStack
{
LinkStackPrt top;//top指针
int count;//栈元素计数器
}
- push栈
Status Push(LinkStack *s, ElemType e)
{
LinkStackPtr p = (LinkStackPtr)malloc(sizeof(StackNode));
p->data = e;
p->next = s->top;
s->top =p;
s->count++;
return OK;
}
- 逆波兰表达式
typedef double ElemType;
typedef struct
{
ElemType *base;
ElemType *top;
int stackSize;
}sqStack;
InitStack(sqStack *s)
{
s->base = (ElemType *)malloc(STACK_INIT_SIZE * sizeof(ElemType));
if( !s->base )
exit(0);
s->top = s->base;
s->stackSize = STACK_INIT_SIZE;
}
Push(sqStack *s, ElemType e)
{
// 栈满,追加空间,鱼油必须懂!
if( s->top - s->base >= s->stackSize )
{
s->base = (ElemType *)realloc(s->base, (s->stackSize + STACKINCREMENT) * sizeof(ElemType));
if( !s->base )
exit(0);
s->top = s->base + s->stackSize;
s->stackSize = s->stackSize + STACKINCREMENT;
}
*(s->top) = e; // 存放数据
s->top++;
}
Pop(sqStack *s, ElemType *e)
{
if( s->top == s->base )
return;
*e = *--(s->top); // 将栈顶元素弹出并修改栈顶指针
}
int StackLen(sqStack s)
{
return (s.top - s.base);
}
int main()
{
sqStack s;
char c;
double d, e;
char str[MAXBUFFER];
int i = 0;
InitStack( &s );
printf("请按逆波兰表达式输入待计算数据,数据与运算符之间用空格隔开,以#作为结束标志: \n");
scanf("%c", &c);
while( c != '#' )
{
while( isdigit(c) || c=='.' ) // 用于过滤数字
{
str[i++] = c;
str[i] = '\0';
if( i >= 10 )
{
printf("出错:输入的单个数据过大!\n");
return -1;
}
scanf("%c", &c);
if( c == ' ' )
{
d = atof(str);
Push(&s, d);
i = 0;
break;
}
}
switch( c )
{
case '+':
Pop(&s, &e);
Pop(&s, &d);
Push(&s, d+e);
break;
case '-':
Pop(&s, &e);
Pop(&s, &d);
Push(&s, d-e);
break;
case '*':
Pop(&s, &e);
Pop(&s, &d);
Push(&s, d*e);
break;
case '/':
Pop(&s, &e);
Pop(&s, &d);
if( e != 0 )
{
Push(&s, d/e);
}
else
{
printf("\n出错:除数为零!\n");
return -1;
}
break;
}
scanf("%c", &c);
}
Pop(&s, &d);
printf("\n最终的计算结果为:%f\n", d);
return 0;
}
- 利用中缀表达式转换为后缀表达式(1+(2-3)4+10/5)
123-4+105/+
114*+105/+
14+105/+
14+2+
52+
7
顺序栈&链栈的异同
-
同(时间复杂度)
顺序栈和链栈的时间复杂度均为O(1) -
异(空间性能)
a.顺序栈
顺序栈需要事先确定一个固定的长度(数组长度)
可能存在内存空间浪费问题,但它的优势是存取时定位很方便
b.链栈
要求每个元素都要配套一个指向下个结点的指针域增大了内存开销,但好处是栈的长度无限因此,如果栈的使用过程中元素变化不可预料,有时很小,有时很大,那么最好使用链栈反之,如果它的变化在可控范围内,则建议使用顺序栈
三、栈的内部实现原理
- 栈的内部实现原理其实就是数组或链表的操作而之所以引入 栈 这个概念,是为
将程序设计问题模型化用高层的模块指导特定行为(栈的先进后出特性),划分了
不同关注层次,使得思考范围缩小更加聚焦于我们致力解决的问题核心,简化了程
序设计的问题
四、递归
定义
- 在运行的过程中直接或者间接调用自己称为递归
每个递归定义必须至少有一个条件,使得当满足条件时,递归不再进 - 迭代使用的是循环,递归使用的是选择结构
int Fib(int i)
{
if( i< 2)
return i == 0 ? 0 : 1;
return Fib(i-1) + Fib(i-2);
}
条件
-
- 子问题须与原始问题为同样的事,且更为简单;
- 不能无限制地调用本身,须有个出口,化简为非递归状况处理
- 实例
1.阶乘N!
intfactorial(n)
{
if( O == n) return 1;
else return n*factorial ( n-1 );
}
2.字符串反向输出
void printf()
{
char a;
scanf( "%c" , &a );
if( a!= '#' ) printf();
if( a != '#') printf( "%c" , a );
}
汉诺塔
//将n个盘子从x借助y移动到z
void move(int n, char x, char y, char z)
{
if( 1 == n )
{
printf("%c-->%c\n", x, z);
}
else
{
move(n-1, x, z, y); // 将n-1个盘子从x借助z移动到y上
printf("%c-->%c\n", x, z); // 将第n个盘子从x移到z上
move(n-1, y, x, z); // 将n-1个盘子从y借助x移动到z上
}
}
int main()
{
int n;
printf("请输入汉诺塔的层数: ");
scanf("%d", &n);
printf("移动的步骤如下: \n");
move(n, 'X', 'Y', 'Z');
return 0;
}
斐波那契数列的递归
八皇后问题
字符串
- BF算法的优化就是KMP算法
// 返回子串T在主串S中第pos个字符之后的位置
// 若不存在,则返回0
// T非空,1 <= pos <= strlen(S)
// 注意:我们这里为了表述方便,字符串使用了第一个元素表示长度的方式。
int index( String S, String T, int pos )
{
int i = pos; // i用于主串S中当前位置下标
int j = 1; // j用于子串T中当前位置下标
while( i <= S[0] && j <= T[0] ) // i或j其中一个到达尾部即终止搜索!
{
if( S[i] == T[i] ) // 若相等则继续下一个元素匹配
{
i++;
j++;
}
else // 若失配则j回溯到第一个元素从新匹配
{
i = i-j+2; // i回溯到上次匹配首位的下一个元素,这是效率低下的关键!
j = 1;
}
}
if( j > T[0] )
{
return i - T[0];
}
else
{
return 0;
}
}
五、队列
定义
- 只允许在一端进行插入操作,而在另一端进行删除操作的线性表(先进先出),实现一个队列同样需要顺序表或链表作为基础。
链表结构
- 常用链表来实现
- 代码
队列
typedef struct QNode{
ElemType data;
struct QNode *next;
}QNode,*QueuePrt; //取结点的地址
typedef struct{
QueuePrt front,rear;//队头、尾指针
}LinkQueue;
创建空队列(创建一个头结点,将队列的头指针和尾指针都指向这个生成的头结点)
initQueue(LinkQueue *q)
{
q->front = q->rear = (QueuePtr)malloc(sizeof(QNode));
if( !q ->front)
exit(0);
q->front->next = NULL;
}
入队列
InsertQueue(LinkQueue *q,ElemType e)
{
QueuePtr p;
p = (QueuePtr)malloc(sizeof(QNode));
if( p == NULL)
exit(0);
p->data = e;
p->next = NULL;
q->rear->next = p;
q->rear = p;
}
出队列
DeleteQueue(LinkQueue *q ,ElemType *e)
{
QueuePtr p;
if( q->front == q->rear )
return;
p = q->front->next;
*e = p->data;
q->front->next = p->next;
if( q->rear == p )
q->rear = q->front;
free(p);
}
销毁队列(由于链队列建立在内存的动态区,不用时应当及时销毁,以免过多占用内存空间)
DestroyQueue(LinkQueue *q)
{
while( q->front ){
q->rear = q->front->next;
free( q->front );
q->front = q->rear;
}
}
顺序结构
- 会出现数组越界的错误(方法:头尾相接的循环)
循环队列
-
循环队列的实现只需要灵活改变front和rear指针即可
-
也就是让front或rear指针不断加1,即使超出了地址范围,也会自动的从头开始。我们采用取模运算处理。
(rear + 1 )% QueueSize
( front+ 1 ) %QueueSize
-
定义一个循环队列
#define MAXSIZE 100
typeof struct
{
ElemType *base;//用于存放内存分配基地址
//可以用数组存放
int front;
int rear;
} -
初始化循环队列
initQueue(cycleQueue *q,ElemType e)
{
q->base = (ElemType *)malloc (MAXSIZE * sizeof(ElemType));
if( !q->base )
exit(0);
q->front = q->next =0;
} -
入栈
InsertQueue(cycleQueue *q,ElemType e)
{
if((q->rear+1)%MAXSIZE == q->front)
return;
q->base[q->rear] = e;//数组形式
q->rear = ( q->rear +1) % MAXSIZE;//(+1)
}
- 出栈
DeleteQueue(cycleQueue *q,ElemType *e)
{
if( q->front == q->rear )
return;
*e = q->base[q->front];
q->front = ( q->front +1) % MAXSIZE;//(+1)
}
六、KMP
- 避免重复遍历的情况(毛片算法)
#include <stdio.h>
typedef char* String;
void get_next( String T, int *next )
{
int j = 0;
int i = 1;
next[1] = 0;
while( i < T[0] )
{
if( 0 == j || T[i] == T[j] )
{
i++;
j++;
if( T[i] != T[j] )
{
next[i] = j;
}
else
{
next[i] = next[j];
}
}
else
{
j = next[j];
}
}
}
// 返回子串T在主串S第pos个字符之后的位置
// 若不存在,则返回0
int Index_KMP( String S, String T, int pos )
{
int i = pos;
int j = 1;
int next[255];
get_next( T, next );
while( i <= S[0] && j <= T[0] )
{
if( 0 == j || S[i] == T[j] )
{
i++;
j++;
}
else
{
j = next[j];
}
}
if( j > T[0] )
{
return i - T[0];
}
else
{
return 0;
}
}
第五章 树
一、定义
- n个结点的有限集。当n=0时称为空树,
二、特点(根节点–子树–子节点)
- 在任意一颗非空树中:
(1)且仅有一个特定的结点:根结点(Root)
(2)当 n>1 时,其余结点可分为m(m>0)个互不相交的有限集
其中每一个集合本身又是一棵树,并且称为根的 子树(SubTree)
(3)根结点大于 0 时根结点是唯一的,不可能同时存在多个根结点
(4)子结点大于 0时,子树的个数没有限制,但它们一定是互不相交的
(5) 结点拥有的子树称为结点的度,树的度取树内各结点的度的最大值。
三、线性结构&树结构的区别用法
(1)树的存储结构
- 双亲表示法
代码
//树的双亲表示法结点结构定义
#define MAX_TREE_SIZE 100
typedef int ElemType;
typedef struct PTNode
{
ElemType data;//节点数据
int parent;//双亲位置
} PINode;
typedef struct
{
PINode nodes[MAX_TREE_SIZE];
int r;// 跟的位置
int n;//节点数目
}PTREE;
//根据结点的parent指针找到它的双亲结点,所用时间复杂度是O(1),要寻找某结点的孩子时,需遍历整个树结构
-
孩子表示法
-
双亲孩子表示法
#define MAX_TREE_SIZE 100
typedef char ElemType;
// 孩子结点
typedef struct CTNode
{
int child; // 孩子结点的下标
struct CTNode *next; // 指向下一个孩子结点的指针
} *ChildPtr;
// 表头结构
typedef struct
{
ElemType data; // 存放在树中的结点的数据
int parent; // 存放双亲的下标
ChildPtr firstchild; // 指向第一个孩子的指针
} CTBox;
// 树结构
typedef struct
{
CTBox nodes[MAX_TREE_SIZE]; // 结点数组
int r, n;
}
四、二叉树
(1)定义
是n个结点的有限集合,该集合或者为空集(空二叉树),或者有一个根据点和两颗互不相交的。分别称为根节点的左子树和右子树的二叉树组成。
(2)特点
A:每个结点最多只能有两棵子树
B:左子树和右子树是有顺序的,次序不能任意颠倒
C: 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树
(3)特殊二叉树
A:满二叉树(所有分支结点都存在左右子树,并且所有叶子都在同一层)
B:完全二叉树(对于具有n个结点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点位置完全相同)
(4)二叉树的存储结构
顺序存储结构
- 顺序存储结构就是用一维数组存储二叉树中的各个结点,并且结点的存储位置能体现结点之间的逻辑关系。
- 把不存在的结点用‘^’代替即可
- 不利于斜树,适用性不强
链式存储结构
- 设计一个数据域和两个指针域,我们称这样的链表为二叉链表
五、五种基本形态
A:空二叉树
B:只有一个跟结点
C:根结点只有左子树
D:根结点只有右子树
E:根结点既有左子树又有右子树
六、五种特性
A:在二叉树的第i层上至多有2^(i-1)个结点
B:深度为k的二叉树至多有2^k - 1个结点
C:对任何一颗二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1
D:具有n个结点的完全二叉树的深度为k= 【log2N】+1 ; 第二层满二叉树的结点数为n=N=2^(k-1)-1 ; 完全二叉树的结点数的取值范围是:2^(k-1)-1<n<=2 ^k -1
E:如果对一颗有n各结点的完全二叉树(其深度为|log2N+1|的结点按层序编号,对任一结点i(1<=i<=n))有以下性质:
a:如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点【i/2】
b:如果2i>n,则结点i无左孩子;否则其左孩子是结点2i;
c:如果2i + 1>n,则结点i无右孩子;否则右孩子结点为2i+1;
七、二叉树的四种遍历
- 是指从根节点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次(次序和访问)
前序遍历
- 若二叉树为空,则空操作返回,否则先访问根节点,然后前序遍历左子树,在前序遍历右子树
- 代码
- abcde代表左斜树,所以输入时要有空格
#include <stdio.h>
#include <stdlib.h>
typedef char ElemType;
typedef struct BiTNode
{
char data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
// 创建一棵二叉树,约定用户遵照前序遍历的方式输入数据
CreateBiTree(BiTree *T)
{
char c;
scanf("%c", &c);
if( ' ' == c )
{
*T = NULL;
}
else
{
*T = (BiTNode *)malloc(sizeof(BiTNode));
(*T)->data = c;
CreateBiTree(&(*T)->lchild);
CreateBiTree(&(*T)->rchild);
}
}
// 访问二叉树结点的具体操作,你想干嘛?!
visit(char c, int level)
{
printf("%c 位于第 %d 层\n", c, level);
}
// 前序遍历二叉树
PreOrderTraverse(BiTree T, int level)
{
if( T )
{
visit(T->data, level);
PreOrderTraverse(T->lchild, level+1);
PreOrderTraverse(T->rchild, level+1);
}
}
int main()
{
int level = 1;
BiTree T = NULL;
CreateBiTree(&T);
PreOrderTraverse(T, level);
return 0;
}
中序遍历
- 若树为空,则空操作返回,否则从根节点开始(注意并不是先访问根节点),中序遍历根节点的左子树,然后是访问根节点,最后中序遍历右子树。
- 线索二叉树********
代码
#include <stdio.h>
#include <stdlib.h>
typedef char ElemType;
// 线索存储标志位
// Link(0):表示指向左右孩子的指针
// Thread(1):表示指向前驱后继的线索
typedef enum {Link, Thread} PointerTag;
typedef struct BiThrNode
{
char data;
struct BiThrNode *lchild, *rchild;
PointerTag ltag;
PointerTag rtag;
} BiThrNode, *BiThrTree;
// 全局变量,始终指向刚刚访问过的结点
BiThrTree pre;
// 创建一棵二叉树,约定用户遵照前序遍历的方式输入数据
void CreateBiThrTree( BiThrTree *T )
{
char c;
scanf("%c", &c);
if( ' ' == c )
{
*T = NULL;
}
else
{
*T = (BiThrNode *)malloc(sizeof(BiThrNode));
(*T)->data = c;
(*T)->ltag = Link;
(*T)->rtag = Link;
CreateBiThrTree(&(*T)->lchild);
CreateBiThrTree(&(*T)->rchild);
}
}
// 中序遍历线索化
void InThreading(BiThrTree T)
{
if( T )
{
InThreading( T->lchild ); // 递归左孩子线索化
if( !T->lchild ) // 如果该结点没有左孩子,设置ltag为Thread,并把lchild指向刚刚访问的结点。
{
T->ltag = Thread;
T->lchild = pre;
}
if( !pre->rchild )
{
pre->rtag = Thread;
pre->rchild = T;
}
pre = T;
InThreading( T->rchild ); // 递归右孩子线索化
}
}
void InOrderThreading( BiThrTree *p, BiThrTree T )
{
*p = (BiThrTree)malloc(sizeof(BiThrNode));
(*p)->ltag = Link;
(*p)->rtag = Thread;
(*p)->rchild = *p;
if( !T )
{
(*p)->lchild = *p;
}
else
{
(*p)->lchild = T;
pre = *p;
InThreading(T);
pre->rchild = *p;
pre->rtag = Thread;
(*p)->rchild = pre;
}
}
void visit( char c )
{
printf("%c", c);
}
// 中序遍历二叉树,非递归
void InOrderTraverse( BiThrTree T )
{
BiThrTree p;
p = T->lchild;
while( p != T )
{
while( p->ltag == Link )
{
p = p->lchild;
}
visit(p->data);
while( p->rtag == Thread && p->rchild != T )
{
p = p->rchild;
visit(p->data);
}
p = p->rchild;
}
}
int main()
{
BiThrTree P, T = NULL;
CreateBiThrTree( &T );
InOrderThreading( &P, T );
printf("中序遍历输出结果为: ");
InOrderTraverse( P );
printf("\n");
return 0;
}
后序遍历
- 若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后访问根节点。
层序遍历
- 若树为空,则空操作返回,否则从树的第一层,也就是根节点开始访问,从上到下逐层遍历,在同层中,按从左到右的顺序对结点逐个访问。
森林和二叉树的转换
-
树到二叉树
-
森林到二叉树
- 二叉树到树、森林
八、赫夫曼编码
- 给定n个权值作为n个叶子结点构造一棵二叉树,若树的带权路径长度达到最小,则这棵树被称为哈夫曼树
- 定长编码:像ASCII编码
- 边长编码:单个编码的长度不一致,可以根据整体出现频率来调节
- 前缀码:所谓的前缀码:就是没有任何码子是其他码子的前缀
(1)路径和路径长度
- 在一棵树中,从一个结点往下可以达到孩子或者孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根节点的层数为1,则从根结点到第L层结点的路径长度为L-1.
(2)结点的权及带权路径长度
- 若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
(3)树的带权路径长度
- 树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。
(4)哈夫曼树图例
图a:WPL = 5 * 2 +7 *2 +2 * 2 + 13 * 2 =54
图b:WPL = 5 * 3 + 2 * 3 +7 * 2 + 13 * 1 = 48
- 可见,图b的带权路径长度较小,我们可以证明图b就是哈夫曼树(最优二叉树)
(5)哈弗曼树构建方式
- 1.将所有左,右子树都为空的作为根节点。
- 2.在森林中选出两颗根节点的权值最小的树作为一颗新树的左,右子树,且置新树的附加根节点的权值为其左,右子树上根节点的权值之和。注意,左子树的权值应小于右子树的权值。
- 3.从森林中删除着两颗树,同时把新树加入森林中,
- 4.重复2,3步骤,知道森林中只有一颗树位置,次树便是哈弗曼树
(6)哈夫曼编码
-
树中从根到每个叶子节点都有一条路径,对路径上的各分支约定指向左子树的分支表示“0”码,指向右子树的分支表示“1”码,取每条路径上的“0”或“1”的序列作为哥哥叶子节点对应的字符编码,即是哈夫曼编码:
-
A,B,C,D对应的哈夫曼编码分别为111,10,110,0
(7)代码实现*****
第五十三讲 赫夫曼编码C语言实现
第六章 图
(1)定义
- 是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中表示一个图,V是图G中顶点的集合,E是图G中边的集合。
- A:在线性表中
数据元素之间是被串起来的,仅有线性关系
每个数据元素只有一个直接前驱和一个直接后驱
- B:在树形结构中
数据元素之间有着明显的层次关系
并且每一层上的数据元素可能和下一层中多个元素相关
但只能和上一层中一个元素相关
- C:图是一种较线性表和树更加复杂的数据结构
在图形结构中,结点之间的关系可以是任意的
图中任意两个数据元素之间都可能相关
(2)线性表&树&图的【数据元素名称-有无结点-内部之间的关系】区别
元素名称 有无结点 内部关系
线性表 元素 无元素->空表 – 相邻元素有线性关系
树 结点 无结点–>空树 --相邻两层的结点有层次关系
图 顶点 不允许没有顶点 --任意两个顶点之间都可能存在关系;顶点之间的逻辑关系用边进行表示;边集可以是空的。
(3)图的五种种类【无向图-有向图-简单图-完全无向图-有向完全图】
无向图
- 定义
若顶点 vi 到 vj 之间的边没有方向,则称这条边为 无向边(Edge)
用无序偶对(vi,vj) 来表示。
如果图中任意两个顶点之间的边都是无向边,则称该图为 无向图
无向图顶点的边数叫做度
顶点集合:V1={ABCD};
边集合: E1=(AB)(BC)(CD)(DA)(AC);
有向图
简单图
无向完全图
有向完全图
稀疏图和稠密图
图的顶点与边之间的关系
-
有向图
-
无向图
生成树
- 连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,并且是只有一棵树的n-1条边,都是连通的。
有向树
- 如果一个有向图恰有一个顶点入度为0,其顶点的入度均为1
图的存储结构
- 因为任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理位置表示元素之间的关系(内存物理位置是线性的,图的元素关系是平面的)
邻接矩阵(无向图)
- 一维数组存储图中顶点信息(不区分大小、主次),一个二维数组存储图中的边或弧的信息(顶点与顶点关系)
邻接矩阵(有向图)
- 有弧表示1,无弧表示0
- 顶点入度之和 = 顶点列的各数之和
- 顶点出度之和 = 顶点行的各数之和
邻接矩阵(网)
-每条边上带有权的图叫网(无穷:表示一个计算机允许的,大于所有边上权值的值)
邻接表(无向图)一个表
- 顶点用一个一维数组存储,顶点可以用单链表来存储,每个顶点Vi的所有临界点构成一个线性表(单链表形式)
邻接表(有向图)两个表
- 邻接表
- 逆邻接表
十字链表
邻接多重表
边集数组
(4)图的两种遍历【深度优先遍历-广度优先遍历】的区别用法(遍历可能重复走过某个顶点或漏了某个顶点的遍历过程)
深度优先遍历DFS
- 右手原则:在没有碰到重复顶点的情况下,分叉路口始终是向右手边走,每路过一个顶点就做一个记号。
- 遍历到了之后,就变为蓝色
广度优先遍历BFS********
- 利用队列来实现,一层层遍历
(5)最小生成树连用网的两张算法区别和用法【普里姆算法-克鲁斯卡尔算法】
普里姆算法
克鲁斯卡尔算法*******
(6)最短路径【迪杰斯特拉算法&弗洛伊德算法】
迪杰斯特拉算法********
- 时间复杂度O(n^2)
弗洛伊德算法
-
一个无环的有向图称为无环图(DAG)
-
在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称之为AOV网
-
时间复杂度O(n^3)
(7)拓扑排序
(8)关键路径
第七章 查找
一、查找表
二、关键字
(1)键值
(2)关键码
(3)关键字
(4)次关键字
三、查找
(1)定义
(2)静态查找表&动态查找表的区别用法【顺序查找-线性查找-折半查找-二分查找-插值查找-斐波那契查找-索引顺序表查找-分块查找】
四、索引
(1)定义
(2)按照结构分类
(3)三种线性索引&两种表【稠密索引-分块索引-多重表-倒排表】
五、二叉排序树【二叉查找树】
(1)定义
(2)目的
(3)存储方式
(4)平衡二叉树
(5)二横因子BF
(6)最小不平衡子树
(7)平衡二叉树实现原理
六、多路查找树
(1)分析
(2)定义
(3)四种常见的多路查找树【2-3树 2-3-4树 B树 ¥B+树】的区别用法
七、散列技术
(1)定义
(2)散列表【哈希表】
(3)散列地址
(4)散列函数设计原则
(5)构造散列函数的六种方法【直接定址法-数字分析法-平方取中法-折叠法-除留余数法-随机数法】
(6)处理散列冲突的四种方法【开放定址法-再散列函数法-链地址法-公共溢出区法】
(7)散列表性能分析
- A:散列函数是否均匀
- B:处理冲突的方法
- C:散列表的装填因子
第八章 内部排序
一、定义
二、多个关键的排序最终都可以转化为单个关键字的排序
三、排序的稳定性
四、内排序&外排序
(1)内排序
(2) 外排序
五、影响内排序的三个方面
(1)时间性能
A:比较
B:移动