[toc]
数据结构
是一门独立的学科, 也可以看出它在计算机专业的地位. 它不是算法的附属品. 好的程序数据结构设计 = 数据结构 + 算法.
数据结构核心名称解释
- 数据 :程序的操作对象,用于描述客观事物.
- 可以输入到计算机
- 可以被计算机处理
- 数据对象:一个数据元素由若干数据项组成
- 数据元素 :组成数据对象的基本单位
- 数据项 :性质相同的数据元素的集合(类似于数组)
- 结构: 数据元素之间不是独立的,存在特定的关系.这些关系即是结构;
- 数据结构:指的数据对象中的数据元素之间的关系
举例子:
- struct Teacher为一种数据结构,里面申明的变量为数据项
//声明一个结构体类型
struct Teacher{ //一种数据结构
char *name; //数据项--名字
char *title; //数据项--职称
int age; //数据项--年龄
};
- 声明一个类型为Teacher的结构体变量T1,此时T1就是数据元素
- 如果把多个数据元素放在一个数组中,数组就成为数据组对象
struct Teacher t1; //数据元素;
struct Teacher tArray[10]; //数据对象;
t1.age = 18; //数据项
t1.name = "teacher"; //数据项
t1.title = "教师"; //数据项
关系图
- 数据对象包含数据元素,数据元素含义数据项
-
数据对象也属于一种数据
逻辑结构和物理结构区别
逻辑结构
- 集合结构
- 数据属于同一个集合,之前没有先后顺序.
- 线性结构
- 线性表 队列 栈 数组
- 数组1对1
- 队列 栈特殊线性结构,在于读取方式,一个先进先出,一个先进后出.
- 字符串也属于,是由一个个字符拼接的
- 树形结构
- 1对多,属于一对多都属于树形结构.
- 不包括线性结构.
- 图形结构
- 多对多
物理结构
- 逻辑结构
数据与数据之前的逻辑关系,这些最终要物理结构存储到内存空间,内存空间存储.
- 物理结构
- 顺序存储,缺点插入比较耗时.插入线位移,在插入.
3 . 链式存储
- 有几个数开辟几个空间,不需要提前开启很多空间.
- 插入找到为位置就行,缺点结构很大,查询起来麻烦.
算法时间复杂度计算
数据结构和算法是不可分割,算法是解决问题,解决问题需要数据.
算法定义
算法就是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列列, 并且每个指令表示⼀一个或多个操作.
算法的特性
- 输⼊入输出
- 有穷性
- 确定性
- 可行性
算法设计要求
- 正确性
- 可读性
- 健壮性
- 时间效率⾼高和储存量量低
算法时间复杂度
- 算法输入时间
- 编译执行时间
- 执行命令
- 执行重复命令
大O表示法
- 用常数1取代运行时间中所有常数
3->1 O(1)
- 在修改运行次数函数中,只保留最高阶项
n^3+2n^2+5 -> O(n^3)
- 如果在最高阶存在且不等于1,则去除这个项目相乘的常数
2n^3 -> n^3
时间复杂度常用术语
- 常数阶
1+1+1 = 3 O(1)
函数执行时间和n无关
void testSum1(int n){
int sum = 0; //执行1次
sum = (1+n)*n/2; //执行1次
printf("testSum1:%d\n",sum);//执行1次
}
1+1+1+1+1+1+1 = 7 O(1) 代码执行固定,不会变
void testSum2(int n){
int sum = 0; //执行1次
sum = (1+n)*n/2; //执行1次
sum = (1+n)*n/2; //执行1次
sum = (1+n)*n/2; //执行1次
sum = (1+n)*n/2; //执行1次
sum = (1+n)*n/2; //执行1次
printf("testSum2:%d\n",sum);//执行1次
}
- 线性阶
函数执行次数有n决定,所以是线性级,所以时间复杂度 O(n)
void add2(int x,int n){
for (int i = 0; i < n; i++) {
x = x+1;
}
}
1+(n+1)+n+1 = 3+2n -> 保留最高阶 O(n)
void testSum3(int n){
int i,sum = 0; //执行1次
for (i = 1; i <= n; i++) { //执行n+1次
sum += i; //执行n次
}
printf("testSum3:%d\n",sum); //执行1次
}
- 平方阶
x=x+1; 执行n*n次 ->O(n^2)
void add3(int x,int n){
for (int i = 0; i< n; i++) {
for (int j = 0; j < n ; j++) {
x=x+1;
}
}
}
1+(n+1)+n(n+1)+n2+n2 = 2+3n^2+2n -> O(n^2)
void testSum5(int n){
int i,j,x=0,sum = 0; //执行1次
for (i = 1; i <= n; i++) { //执行n+1次
for (j = 1; j <= n; j++) { //执行n(n+1)
x++; //执行n*n次
sum = sum + x; //执行n*n次
}
}
printf("testSum5:%d\n",sum);
}
- 对数阶
观察值当2的n吃饭不小于n循环结束
因为2的x次方等于n x = log2n ->O(logn)
void testA(int n){
int count = 1; //执行1次
//n = 10
while (count < n) {
count = count * 2;
}
}
- 立方阶
void testB(int n){
int sum = 1; //执行1次
for (int i = 0; i < n; i++) { //执行n次
for (int j = 0 ; j < n; j++) { //执行n*n次
for (int k = 0; k < n; k++) {//执行n*n*n次
sum = sum * 2; //执行n*n*n次
}
}
}
}
- nlog阶
- 后面再讲
- 指数阶(不考虑)
O(2^n)
或者O(n!)
除非是非常小的n
,否则会造成噩梦般的时间消耗. 这是一种不切实际的算法时间复杂度. 一般不考虑!
图示
空间复杂度
算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式 记做: S(n) = n(f(n)),其中,n为问题的规模,f(n)为语句句关于n所占存储空间的函数
- 主要考虑算法执行时所需要的辅助空间
开辟一个空间 o(1)
算法实现(1)
int temp;
for(int i = 0; i < n/2 ; i++){
temp = a[I];
a[i] = a[n-i-1];
a[n-i-1] = temp;
}
开辟空间去决定n O(n)
int b[10] = {0};
for(int i = 0; i < n;i++){
b[i] = a[n-i-1];
}
for(int i = 0; i < n; i++){
a[i] = b[I];
}
线性表 顺序表
- 对于⾮非空的线性表和线性结构,其特点如下:
- 线性表:零个或者多个数据元素的有限序列。元素之间是有序的,若元素存在多个,则第一个元素无前驱,最后一个元素无后继。线性表是有限的)
- 线性表的个数定义为线性表的长度 个数为0即是空表。
3 . 在复杂的线性表中,一个数据元素可以由若干个数据项组成。
- 线性表的顺序存储结构: 指的是用一段地址连续的存储单元依次存储线性表的数据元素。
图示
顺序存储
声明一下宏定义,结构体
#define MAXSIZE 100
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
/* ElemType类型根据实际情况而定,这里假设为char */
typedef char ElemType;
/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int Status;
/*线性结构使用顺序表的方式存储*/
typedef struct{
ElemType *data;
int length;
}Sqlist;
顺序表初始化
Status InitList(Sqlist *L){
//为顺序表分配一个大小为MAXSIZE 的数组空间
// L->data指向首地址
L->data = malloc(sizeof(ElemType)*MAXSIZE);
//存储分配失败退出
if (!L->data) return ERROR;
//空表长度为0
L->length = 0;
return OK;
}
- 注意:需要注意的是:
- 数组的长度是存放线性表的存储空间的长度。
- 线性表的长度是线性表中数据元素的个数,随着线性表的增删改查,线性表的长度是有变化的。
- 在任意时刻,线性表的长度该小于等于数组的长度。
顺序表插入
初始条件:顺序线性表L已存在,1≤i≤ListLength(L)
;
操作结果:``中第i个位置之前插入新的数据元素e,L的长度加1
注意index
下标从0
开始,i
从1
开始 ,i
开始i-1
哪个下标之后
思路:
1.插入位置不合理,抛出异常。例如数组的长度为20
,线性表的长度为10
,插入的位置是15
,显然不合规矩的,顺序线性链表中的元素是连续的。
2.如果未向线性表中插入数据之前,线性表的长度等于数组长度(不可能大于),则抛出异常或动态增加容量。
3.从第i个位置遍历到最后一个元素,将这些元素都向后移动一个位置。
4.将要插入的元素放置到位置i处。
5.线性表长length加一。
时间复杂度 O(n)
Status ListInset(Sqlist *L,int i,ElemType e){
////线性表已填满
if (L->length > SIZE_MAX) return ERROR;
//length == 100 i==101,102就是越界
////插入位置不合理,抛出异常。
if(i<1 || (i > L->length +1)) return ERROR;
//插入数据不在表尾,则先移动出空余位置
//i <= L->length 从最后一个移动
if (i<=L->length){
// L->length 是下标加一 , L->length-1刚好最后一位下标
for(int j=L->length-1;j>=i-1;j--){
//插入位置以及之后的位置后移动1位
L->data[j+1] = L->data[j];
}
}
//将新元素e 放入第i个位置上
L->data[i-1] = e;
//长度+1;
++L->length;
return OK;
}
顺序表的取值
思路:
1.线性表不能为空。
2.下标不能小于1。
3.下标i不能大于线性表长。
时间复杂度:O(1)
Status GetElem(Sqlist L,int i,ElemType *e){
//判断i值是否合理, 若不合理,返回ERROR
if (i<1 || i>L.length) return ERROR;
//data[i-1]单元u存储第i个数据元素
*e = L.data[i-1];
return OK;
}
顺序表删除
思路:
1.如果删除位置不合理,抛出异常。例如数组的长度为20,线性表的长度为10,删除的位置是15,显然不合规矩的,顺序线性链表中的元素是连续的。
2.取出删除的元素。
3.从删除元素的位置开始遍历到最后一个元素结束,分别将他们都向前移动1个位置。
4.线性表长length减一。
时间复杂度 O(n)
Status DeleElem(Sqlist *L,int i,ElemType *e){
if (L->length == 0) return ERROR;
if (i<1 || (i>L->length+1)) return ERROR;
// 删除第i个元素,
for(int j=i;j<L->length;j++){
//被删除元素之后的元素向前移动
L->data[j-1] = L->data[j];
}
//表长度-1;
--L->length;
return OK;
}
清空顺序表
初始条件:顺序线性表L已存在。操作结果:将L重置为空表
Status ClearList(Sqlist *L){
L->length = 0;
return OK;
}
判断顺序表清空
Status ListEmpty(Sqlist L){
if(L.length == 0){
return TRUE;
}
return FALSE;
}
获取顺序表长度ListEmpty元素个数 */
Status ListLength(Sqlist L){
return L.length;
}
顺序输出List
初始条件:顺序线性表L已存在
操作结果:依次对L的每个数据元素输出
Status TraverseList(Sqlist L){
for(int i=0;i<L.length;i++){
printf("%c\n",L.data[I]);
}
printf("\n");
return OK;
}
顺序表查找元素并返回位置
初始条件:顺序线性表L已存在
操作结果:返回L
中第1
个与e
满足关系的数据元素的位序。
若这样的数据元素不存在,则返回值为0
int LocateElem(Sqlist L,ElemType e){
int I;
if (L.length == 0) return ERROR;
for (i=0; i<L.length; i++) {
if (L.data[i] == e){
break;
}
}
if (i>=L.length) return 0;
return I+1;
}
顺序表 元素是否在线性顺序表中
元素是否在线性顺序表中
@param list 线性顺序存储表
@param elem 元素
@return ERROR 不存在;SUCCESS 存在
Status ElemisExitInList(Sqlist L,ElemType e){
if (L.length == 0) return ERROR;
for (int i=0; i<L.length; i++) {
if (L.data[i] == e){
return OK;
}
}
return ERROR;
}
两个线性表的合并 没有重复的元素。
假设Lb向L合并,这里面涉及一个扩容问题
memcpy
(data, L->data, sizeof(ElemType) * 2 * MAXSIZE);
Status combineTwoLinkLists(Sqlist *L,Sqlist L1){
int LLength = L->length;
int L1Length = L1.length;
if (LLength + L1Length > LLength){
ElemType* data = malloc(sizeof(ElemType) * 2 * MAXSIZE);
//从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中
memcpy(data, L->data, sizeof(ElemType) * 2 * MAXSIZE);
free(L->data);
L->data = data;
if(!L->data) exit(ERROR);
}
for (int i=0; i<L1Length; i++) {
ElemType e = L1.data[I];
Status isState = ElemisExitInList(*L, e);
if (isState == ERROR){
ListInsert(L, LLength-1, e);
}
}
TraverseList(*L);
return OK;
}
最后插入数据时候 可以将
MAXSIZE*2
看看是否全部打印
- 示例
Sqlist L;
ElemType e;
Status isStatus;
//1.1 顺序表初始化
isStatus = InitList(&L);
printf("初始化L后: L.Length = %d\n", L.length);
//1.2 顺序表插入数据
char a[6] = {'a','b','c','d','e','\0'};
char *b = a;
for (int i=1; i<=5; i++) {
isStatus = ListInset(&L,i , *(b+i-1));
}
printf("插入数据L长度: %d\n",L.length);
//1.3 顺序表取值
GetElem(L, 2, &e);
printf("顺序表L第%d个元素的值为:%c\n",2,e);
//1.4 顺序表删除元素
DeleElem(&L, 2, &e);
printf("顺序表删除第%d元素%c,长度为%d\n",2,e,L.length);
TraverseList(L);
//1.5 清空顺序表
isStatus = ClearList(&L);
TraverseList(L);
// 判断list是否为空
isStatus = ListEmpty(L);
printf("L是否空:i=%d(1:是 0:否)\n",isStatus);
for (int i=1; i<=5; i++) {
isStatus = ListInset(&L,i , *(a+i-1));
}
TraverseList(L);
- 打印
初始化L后: L.Length = 0
插入数据L长度: 5
顺序表L第2个元素的值为:b
顺序表删除第2元素b,长度为4
a
c
d
e
L是否空:i=1(1:是 0:否)
a
b
c
d
e
线性表创建已经存在,内存空间已经分配,只要
lengtn=0
,就不会访问.
总结
优点:取出元素很容易。不用为表示表中元素之间的逻辑关系而增加额外的存储空间。
缺点:删除和插入要移动大量元素。删除的时候容易造成存储空间碎片。线性表的长度变化较大时,难以控制存储空间的容量.线性表的数组长度需要给定。
单链表
在顺序结构中,每个数据元素只需要存储数据元素的信息就可以了。在链式存储过程中,除了要存储数据元素的信息外,还要存储它的后继元素的存储地址
。把存储数据元素信息的区域称为数据域,把存储直接后继位置成为指针域。指针域中存储的信息称为指针或者链
。指针域和数据域两部分信息组成数据元素的存储映像,称为结点。
定义:单链表:n个结点链组成一个链表,即为线性表的链式存储结构。因为此结点中只包含一个指针域,所以叫单链表
- 结点 数据不联系,数据之间通过指针域联系.
- 第一个结点首元结点,最有一个指针域为NULL
- 单链表后面的元素找不到前面的.
链表中的第一个结点的存储位置称为头指针。最后一个结点的指针为空。
单链表的第一个结点前附设一个结点,称为头结点
- 头结点和头指针的区分
头指针是指向链表中第一个结点的指针
。头结点是为了操作的统一方便设立的,放在第一个元素的结点之前。若链表中有头结点
,那么链表中的第一个结点就是头结点了,所以头指针此刻指向的是头结点的指针
。
头指针具有标识作用,所以常用头指针冠以链表的名字
。无论链表是否为空,头指针均不为空。头指针是链表的必要元素。
疑问
:为什么空链表一定要有头指针,个人认为链表为空表示链表存在,但是没有任何结点。头指针此刻更觉得像是存储着空链表的地址。
链表有了头结点
,在对第一元素结点前插入结点和删除第一个结点,其操作与其他结点的操作就统一了。头结点不一定是链表的必需元素。
- 开始行动,做一个单链表
宏定义 ,数据结构
typedef struct Node* LinkList; // 声明一个struct Node*类型的变量
设计链表可以在首元结点前面加一个头结点,首指针指向头结点,方便删除添加,作为一个标记.便于操作首元结点.
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define OK 1
#define MAXSIZE 20 /* 存储空间初始分配量 */
typedef int Status;/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int ElemType;/* ElemType类型根据实际情况而定,这里假设为int */
//定义节点
typedef struct Node{
ElemType data;
struct Node *next;//下一个节点
}Node;
typedef struct Node* LinkList; //
初始化单链表线性表
结e点是初始化带入的
Status InitList(LinkList *L){
//产生头结点,并使用L指向此头结点
*L = (LinkList)malloc(sizeof(Node));
//存储空间分配失败
if(*L == NULL) return ERROR;
//将头结点的指针域置空
(*L)->next = NULL;
return OK;
}
单链表插入
初始条件:顺序线性表L已存在,1≤i≤ListLength(L);
操作结果:在L中第i个位置之后插入新的数据元素e,L的长度加1;
-
LinkList *L
L保存的是LinkLis
t地址,(*L)
拿到是LinkList*
,然后才是指向结构体的指针 -
LinkList L
是指向结构体的指针
假设i=4
如果找到插入节点上一个结点,因为每次 p = p->next,查找i-1次,所以要从头结点开始.
Status ListInsert(LinkList *L,int i,ElemType e){
int j;
LinkList p,s;
p = *L;
j = 1;
//寻找第i-1个结点
while (p && j<i) {
//j=4
p = p->next;
++j;
}
//第i个元素不存在
if(!p || j>i) return ERROR;
//生成新结点s
s = (LinkList)malloc(sizeof(Node));
//将e赋值给s的数值域
s->data = e;
//将p的后继结点赋值给s的后继
s->next = p->next;
//将s赋值给p的后继
p->next = s;
return OK;
}
单链表取值
初始条件: 顺序线性表L
已存在,1≤i≤ListLength(L)
;
操作结果:用e
返回L
中第i
个数据元素的值
- 头结点的
next
内元素一次往下遍历
Status GetElem(LinkList L,int i,ElemType *e){
//j: 计数.
int j;
//声明结点p;
LinkList p;
//将结点p 指向链表L的第一个结点;
p = L->next;
//j计算=1;
j = 1;
//p不为空,且计算j不等于i,则循环继续
while (p && j<i) {
//p指向下一个结点
p = p->next;
++j;
}
//如果p为空或者j>i,则返回error
if(!p || j > i) return ERROR;
//e = p所指的结点的data
*e = p->data;
return OK;
}
单链表删除元素
初始条件:顺序线性表L
已存在,1≤i≤ListLength(L)
操作结果:删除L的第i
个数据元素,并用e
返回其值,L
的长度减1
从首元节点开始遍历,所以遍历次次数是
j<(i-1)
,
p = p->next
;相当于提前走一次找到4
的前一个3
位置就要在第2
次停止
Status ListDelete(LinkList *L,int i,ElemType *e){
int j;
LinkList p,q;
p = (*L)->next;
j = 1;
//查找第i-1个结点,p指向该结点
while (p->next && j<(i-1)) {
p = p->next;
++j;
}
//当i>n 或者 i<1 时,删除位置不合理
if (!(p->next) || (j>i-1)) return ERROR;
//q指向要删除的结点
q = p->next;
//将q的后继赋值给p的后继
p->next = q->next; // cc->cooci
//将q结点中的数据给e
*e = q->data; //删除的值
//让系统回收此结点,释放内存;
free(q); //释放q
return OK;
}
遍历显示
初始条件:顺序线性表L已存在
操作结果:依次对L的每个数据元素输出
Status ListTraverse(LinkList L)
{
LinkList p=L->next;
while(p)
{
printf("%d\n",p->data);
p=p->next;
}
printf("\n");
return OK;
}
置空链表
初始条件:顺序线性表L已存在。操作结果:将L重置为空表
Status ClearList(LinkList *L)
{
LinkList p,q;
p=(*L)->next; /* p指向第一个结点 */
while(p) /* 没到表尾 */
{
q=p->next;
free(p);
p=q;
}
(*L)->next=NULL; /* 头结点指针域为空 */
return OK;
}
单链表前插入法
随机产生n个元素值,建立带表头结点的单链线性表L(前插法)
每次循环 都让头结点
next
指向新创建的结点
新创建的结点的next
指向 头结点的next
void CreateListHead(LinkList *L, int n){
LinkList p;
//建立1个带头结点的单链表
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL;
//循环前插入随机数据
for(int i = 0; i < n;i++)
{
//生成新结点
p = (LinkList)malloc(sizeof(Node));
//i赋值给新结点的data
p->data = I;
//p->next = 头结点的L->next,头结点的下一个节点
p->next = (*L)->next;
//(*L)->next有两个指向他,修改一下 让头结点不指向他
//将结点P插入到头结点之后,头结点指向新建的节点
(*L)->next = p;
}
}
单链表后插入法
随机产生n
个元素值,建立带表头结点的单链线性表L
(后插法)
发现后面的数据在最前面
生成头结点,并且给r
,目的保存尾节点
创建一个节点,r
指向她,并保存为尾节点,最后将r
的next
置空.
void CreateListTail(LinkList *L, int n){
LinkList p,r;
//建立1个带头结点的单链表
*L = (LinkList)malloc(sizeof(Node));
//r指向尾部的结点
r = *L;
for (int i=0; i<n; i++) {
//生成新结点
p = (Node *)malloc(sizeof(Node));
p->data = I;
//将表尾终端结点的指针指向新结点
r->next = p;
//将当前的新结点定义为表尾终端结点 r指向尾结点
r = p; // r指向p,
}
//将尾指针的next = null
r->next = NULL;
}
实例
Status iStatus;
LinkList L;
ElemType e;
//2.1 单链表初始化
iStatus = InitList(&L);
printf("L 是否初始化成功?(0:失败,1:成功) %d\n",iStatus);
//2.2 单链表插入数据
for(int j = 1;j<=5;j++)
{
iStatus = ListInsert(&L, 1, j);
}
printf("L 插入后\n");
ListTraverse(L);
//2.3 单链表获取元素
GetElem(L,5,&e);
printf("第5个元素的值为:%d\n",e);
//2.4 删除第5个元素
iStatus = ListDelete(&L, 5, &e);
printf("删除第5个元素值为:%d\n",e);
ListTraverse(L);
//3.1 前插法整理创建链表L
iStatus = ClearList(&L);
CreateListHead(&L, 5);
printf("整理创建L的元素(前插法):\n");
ListTraverse(L);
//3.2 后插法整理创建链表L
iStatus = ClearList(&L);
CreateListTail(&L, 5);
printf("整理创建L的元素(后插法):\n");
ListTraverse(L);
打印
是否初始化成功?(0:失败,1:成功) 1
L 插入后
5
4
3
2
1
第5个元素的值为:1
删除第5个元素值为:1
5
4
3
2
整理创建L的元素(前插法):
4
3
2
1
0
整理创建L的元素(后插法):
0
1
2
3
4
Program ended with exit code: 0