数据结构与算法 总共分为19个系列
①、数据结构与算法[基础]+线性结构部分内容
①、数据结构与算法[基础]+线性结构部分内容篇
⭐️本文章知识点大纲
数据结构核心名称解析
逻辑结构与物理结构区别
算法设计要求
算法效率衡量方法
算法时间复杂度
常见的时间复杂度
算法空间复杂度计算
线性表-关于顺序存储的实现(增删改查)
线性表-关于链式(单链表)存储的设计(增删改查与头插法/尾插法)
⭐️数据结构包含哪些内容
数据结构基本术语
1.1 数据结构
1.2 数据
1.3 数据对象
1.4 数据元素
1.5 数据项2.1 逻辑结构与物理结构
2.2 数据类型与抽象数据类型
①、数据结构基础
1.1 数据结构核心名称解析
数据: 程序的操作对象,用于描述客观事物.
数据的特点: 1、可以输入到计算机 2、可以被计算机处理
数据对象: 性质相同的数据元素的集合(类似于数组)
数据元素: 组成数据的对象的基本单位
数据项: 一个数据元素由若干数据项组成
结构: 数据元素之间不是独立的,存在特定的关系.这些关系即是结构;
数据结构:指的数据对象中的数据元素之间的关系
1.2 数据结构—基本数据单位
1.3. 案例-使用一个C语言结构体
来演练数据结构-基本数据单位
#include <stdio.h>
struct Person{
//一种数据结构
char *name; //数据项--名字
int age; //数据项--年龄
char *professional; //数据项--职业
};
int main(int argc, const char * argv[]) {
struct Person p; //数据元素;
struct Person pArray[10]; //数据对象;
p.name = "宇夜iOS"; //数据项
p.age = 30; //数据项
p.professional = "iOS开发工程师"; //数据项
printf("我的姓名:%s\n",p.name);
printf("我的年龄:%d\n",p.age);
printf("我的职业:%s\n",p.professional);
return 0;
}
2.1 逻辑结构
2.1.1 逻辑结构-集合结构
什么是集合结构:
一班学生、一个学校的老师、一个动物园的动物都是集合结构。
数组、字典是集合结构吗? 不是扫描二维码关注公众号,回复: 13724207 查看本文章
2.1.2 逻辑结构-线性结构(面试重要部分)
什么是线性结构?
所有符合一对一的关系都是线性结构。
例如:
链表、字典、数组(唯一的第一个元素和唯一的最后一个元素)
(特殊的线性结构-特殊点:先进先出、先进后出)队列、栈
(特别的线性结构-特别的就是由字符组成)字符串
线性结构有两个特点:
1.数据和数据之间存在一对一的关系。
线性结构只是表达了它们的逻辑关系
2.1.3 逻辑结构-线性结构-树形-图形(面试重要部分)
树形结构特点:一对多的关系
常见的有:二叉树、B树、多路树、红黑树
图形结构特点:多对多
常见的有:邻接矩阵、邻接表
2.1 总结
上面的结构是逻辑体现。最终还是要存储到内存中间来的。
2.2 物理结构
不管逻辑结构再复杂 最终还是都要存储到内存当中来。
物理结构分为
- 顺序存储结构
- 链式存储结构
2.2.1 顺序存储结构
顺序存储结构的缺点:
比如插入一个数据 需要挪动其他的数据
2.2.2 链式存储结构
链式存储结构的特点:不需要提前开辟一段存储空间。
链式存储结构插入一个数据的时候。找到需要插入的位置进行插入即可
链式存储结构的劣势:查询比较麻烦、遍历比较麻烦
⭐️算法
数据结构与算法关系
两种算法之间比较
算法定义
算法特性
算法设计要求
算法效率衡量方法
算法时间复杂度
常见的时间复杂度
最坏情况与平均情况
算法空间复杂度
②、算法的概念-时间复杂度-空间复杂度
1.数据结构与算法之间的关系
算法是什么
算法是解决问题的一种方法
比如计算1-10的累加。它也是一种算法。
数据结构和算法是不可分割的。
1.1 案例-利用菜市场买菜来说明 数据结构与算法之间的关系
是不可分割的
2. 案例-两种算法之间比较
实现程序: 求得1+2+3+……+100结果的程序?
方式1 for循环操作
int sum , n; n = 100;
sum = 0;
for(int i = 0; i <= n ; i++)
{
sum += i;
}
printf(“%d”,sum);
方式2
int sum = 0 , n = 100;
sum = (1 + n) * n / 2;
printf(“%d”,sum);
3.算法定义
什么是算法?
算法就是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列 ,并且每个指令表示一个或多个操作.
4. 算法的特性
- 输入输出
- 有穷性 ps: 不能无限循环。必须有终止
- 确定性 ps: 必须有确定的结果。
- 可行性 ps: 必须保证每一行代码都是可执行的,每一行代码都是在有限步骤完成。不能出现代码编译出错的情况。
5. 衡量一个算法 - 算法设计要求
正确性 ps: 必须能运行
可读性 ps: 不建议不写注释
健壮性 ps: 不能因为代码动不动挂掉。必须要有提示
时间效率高和储存量低
6. 时间复杂度
- 算法输入时间
- 编译可执行代码
- 执行指令
- 执行重复的指令
7. 计算一个时间复杂度 - 大O表示法
大O表示法以及时间复杂度术语
/*大O表示法规矩
1. 用常数1取代运行时间中所有常数 3->1 O(1)
2. 在修改运行次数函数中,只保留最高阶项 n^3+2n^2+5 -> O(n^3)
3. 如果在最高阶存在且不等于1,则去除这个项目相乘的常数 2n^3 -> n^3
*/
/*
时间复杂度术语:
1. 常数阶
2. 对数阶
3. 线性阶
4. 平方阶
5. 立方阶
6. nlog阶
7. 指数阶(不考虑) O(2^n)或者O(n!) 除非是非常小的n,否则会造成噩梦般的时间消耗. 这是一种不切实际的算法时间复杂度. 一般不考虑!
*/
7.1 案例 - 计算时间复杂度 - 常数阶 - O(1)
/* 1. 常数阶时间复杂度计算 O(1) */
//1+1+1 = 3 O(1)
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次
}
//x=x+1; 执行1次
void add(int x){
x = x+1;
}
7.2 案例- 计算时间复杂度 - 对数阶 - O(logn)
/*3.对数阶*/
/*2的x次方等于n x = log2n ->O(logn)*/
void testA(int n){
int count = 1; //执行1次
//n = 10
while (count < n) {
count = count * 2;
}
}
7.3 案例 - 计算时间复杂度 - 线性阶 O(n)
/*2.线性阶时间复杂度*/
//x=x+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次
}
7.4 案例- 计算时间复杂度 - 平方阶 - O(n^2)
/*4.平方阶*/
//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;
}
}
}
//n+(n-1)+(n-2)+...+1 = n(n-1)/2 = n^2/2 + n/2 = O(n^2)
//sn = n(a1+an)/2
void testSum4(int n){
int sum = 0;
for(int i = 0; i < n;i++)
for (int j = i; j < n; j++) {
sum += j;
}
printf("textSum4:%d",sum);
}
//1+(n+1)+n(n+1)+n^2+n^2 = 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);
}
7.5 案例- 计算时间复杂度 - 立方阶 - O(n^3)
/*5.立方阶*/
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次
}
}
}
}
8. 空间复杂度
在考量算法的空间复杂度,主要考虑算法执行时所需要的辅助空间.
算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式 记做: S(n) = n(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数
/*
程序空间计算因素:
1. 寄存本身的指令
2. 常数
3. 变量
4. 输入
5. 对数据进行操作的辅助空间
*/
8.1 案例 - 空间复杂度 《数组逆序,将一维数组a中的n个数逆序存放在原数组中.》
int main(int argc, const char * argv[]) {
// insert code here...
printf("Hello, World!\n");
int n = 5;
int a[10] = {
1,2,3,4,5,6,7,8,9,10};
//算法实现(1)
// 使用了一个临时变量做中间的变换。
// 空间复杂度 是O(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;
}
for(int i = 0;i < 10;i++)
{
printf("%d\n",a[i]);
}
//算法实现(2)
// 空间复杂度 是O(n)
// 把b[10] 数组代入了循环
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];
}
for(int i = 0;i < 10;i++)
{
printf("%d\n",a[i]);
}
return 0;
}
9 - 衡量一个时间复杂度 - 是考虑最好
的情况 还是 最坏
的情况
衡量一个算法都是考虑最坏的情况
考虑这个算法 最多需要长的时间
如果两个算法都是表示O(n^2) 那么就比较平均值
③、⭐️线性表(面试占比率较多)
线性表包含了:单链表、双向链表、单向循环列表、双向循环列表等
线性表是一种什么数据结构?
一对一的逻辑结构
存在唯一的一个被称作”第一个”的数据元素;
存在唯一的一个被称作”最后一个"的数据元素
除了第一个之外,结构中的每个数据元素均有一个前驱
除了最后一个之外,结构中的每个数据元素都有一个后继.
1.案例 - 线性表 - 顺序存储实现
线性表->顺序存储 (逻辑相邻,物理存储地址相邻)
- 初始化
- 插入
2.1 是否已经存在 插入的位置是否合法
2.2 找到位置 进行位移;然后修改length- 删除
- 清空顺序表
#include <stdio.h>
#include "stdlib.h"
#include "math.h"
#include "time.h"
#define MAXSIZE 100
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
/* ElemType类型根据实际情况而定,这里假设为int */
typedef int ElemType;
/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int Status;
//顺序表结构设计
// *data 为什么要设计一个指针
// 可以为data 开辟一个空间
typedef struct {
ElemType *data; // 数据
int length; // 数组的长度
}Sqlist;
//1.1 顺序表初始化
// 为什么要传递一个指针,因为要操作
Status InitList(Sqlist *L){
//为顺序表分配一个大小为MAXSIZE 的数组空间
L->data = malloc(sizeof(ElemType) * MAXSIZE);
//存储分配失败退出
if(!L->data) exit(ERROR);
//空表长度为0
L->length = 0;
return OK;
}
//1.2 顺序表的插入
/*
初始条件:顺序线性表L已存在,1≤i≤ListLength(L);
操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1
*/
Status ListInsert(Sqlist *L,int i,ElemType e){
// ⭐️这里的1 表示第0个元素作为哨兵
//i值不合法判断
if((i<1) || (i>L->length+1)) return ERROR;
//存储空间已满
if(L->length == MAXSIZE) return ERROR;
//插入数据不在表尾,则先移动出空余位置
if(i <= L->length){
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.3 顺序表的取值
Status GetElem(Sqlist L,int i, ElemType *e){
//判断i值是否合理, 若不合理,返回ERROR
if(i<1 || i > L.length) return ERROR;
//data[i-1]单元存储第i个数据元素.
*e = L.data[i-1];
return OK;
}
//1.4 顺序表删除
/*
初始条件:顺序线性表L已存在,1≤i≤ListLength(L)
操作结果: 删除L的第i个数据元素,L的长度减1
*/
Status ListDelete(Sqlist *L,int i){
//线性表为空
if(L->length == 0) return ERROR;
//i值不合法判断
if((i<1) || (i>L->length)) return ERROR;
for(int j = i; j < L->length;j++){
//被删除元素之后的元素向前移动
L->data[j-1] = L->data[j];
}
//表长度-1;
L->length --;
return OK;
}
//1.5 清空顺序表
/* 初始条件:顺序线性表L已存在。操作结果:将L重置为空表 */
Status ClearList(Sqlist *L)
{
L->length=0;
return OK;
}
//1.6 判断顺序表清空
/* 初始条件:顺序线性表L已存在。操作结果:若L为空表,则返回TRUE,否则返回FALSE */
Status ListEmpty(Sqlist L)
{
if(L.length==0)
return TRUE;
else
return FALSE;
}
//1.7 获取顺序表长度ListEmpty元素个数 */
int ListLength(Sqlist L)
{
return L.length;
}
//1.8 顺序输出List
/* 初始条件:顺序线性表L已存在 */
/* 操作结果:依次对L的每个数据元素输出 */
Status TraverseList(Sqlist L)
{
int i;
for(i=0;i<L.length;i++)
printf("%d\n",L.data[i]);
printf("\n");
return OK;
}
//1.9 顺序表查找元素并返回位置
/* 初始条件:顺序线性表L已存在 */
/* 操作结果:返回L中第1个与e满足关系的数据元素的位序。 */
/* 若这样的数据元素不存在,则返回值为0 */
int LocateElem(Sqlist L,ElemType e)
{
int i;
if (L.length==0) return 0;
for(i=0;i<L.length;i++)
{
if (L.data[i]==e)
break;
}
if(i>=L.length) return 0;
return i+1;
}
int main(int argc, const char * argv[]) {
// insert code here...
printf("Hello, Data Structure!\n");
Sqlist L;
Sqlist Lb;
ElemType e;
Status iStatus;
//1.1 顺序表初始化
iStatus = InitList(&L);
printf("初始化状态 %d\n",iStatus);
printf("初始化L后: L.Length = %d\n", L.length);
//1.2 顺序表数据插入
for(int j=1; j <= 5;j++){
iStatus = ListInsert(&L, 1, j);
}
printf("插入数据L长度: %d\n",L.length);
//1.3 顺序表取值
GetElem(L, 5, &e);
printf("顺序表L第5个元素的值为:%d\n",e);
//1.4 顺序表删除第2个元素
ListDelete(&L, 2);
printf("顺序表删除第%d元素,长度为%d\n",2,L.length);
//1.5 清空顺序表
iStatus = ClearList(&L);
printf("清空后,L.length = %d\n",L.length);
//1.6 判断List是否为空
iStatus=ListEmpty(L);
printf("L是否空:i=%d(1:是 0:否)\n",iStatus);
//1.8 TraverseList
for(int j=1; j <= 5;j++){
iStatus = ListInsert(&L, 1, j);
}
TraverseList(L);
return 0;
}
2. 线性表 - 单链表
2.1 链表的节点
链表会有一个节点
节点 有两个区域
数据域、指针域
线性表的链表-链式存储最大的特点是: 不连续的
线性表能不能通过链表来进行存储? 可以的
链式存储是通过指针域来进行连接的
2.2 单链表的结构
有个(L-首指针)指针指向这个链表
第一个节点叫首元节点
最后一个节点的特点 : 指针域为空。因为后面没有指向的对象了
单链表最后的指针是没有办法找到首节点的
2.案例 - 线性表 - 单链表存储分析
2.0 为什么要使用头节点 好处是什么
单链表为什么使用头节点
- 便于首元节点处理
- 便于空表和非空表的统一处理
2.1 不使用头节点的情况
在插入节点的时候。
- 要判断是不是要插入在首元节点。如果插入到首元节点需要改变L指针的指针域
- 如果插入到其他节点。只需要将上一个节点的指针域指向插入节点的数据域。将插入节点的指针域指向到上一个节点的指针域即可
2.2 使用头节点的情况
不管插入到首元节点还是其他节点
不需要判断是不是插入到首元节点。直接操作上面的步骤《2》即可。
如果插入到其他节点。只需要将上一个节点的指针域指向插入节点的数据域。将插入节点的指针域指向到上一个节点的指针域即可
2.3 单链表插入流程
2.4 单链表删除流程
2.5 单链表头插法流程
2.6 单链表尾插法流程
2.案例 - 线性表 - 单链表存储实现
#include <stdio.h>
#include "string.h"
#include "ctype.h"
#include "stdlib.h"
#include "math.h"
#include "time.h"
#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 */
//定义节点
// 因为Node是一个节点
所有它的data不需要指针包含
// *next是指向下一个结构体
typedef struct Node{
// 数据域
ElemType data;
// 指针域
struct Node *next;
}Node;
typedef struct Node * LinkList;
//2.1 初始化单链表线性表 - 默认带有头节点
Status InitList(LinkList *L){
//⭐️产生头结点,并使用L指向此头结点
*L = (LinkList)malloc(sizeof(Node));
//存储空间分配失败
if(*L == NULL) return ERROR;
//将头结点的指针域置空
(*L)->next = NULL;
return OK;
}
//2.2 单链表插入
/*
初始条件:顺序线性表L已存在,1≤i≤ListLength(L);
操作结果:在L中第i个位置之后插入新的数据元素e,L的长度加1;
*/
Status ListInsert(LinkList *L,int i,ElemType e){
// j的作用 用来计算
int j;
// p用来遍历的 s创建新的节点的
LinkList p,s;
p = *L;
j = 1;
//寻找第i-1个结点
while (p && j<i) {
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;
}
//2.3 单链表取值
/*
初始条件: 顺序线性表L已存在,1≤i≤ListLength(L);
操作结果:用e返回L中第i个数据元素的值
*/
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;
}
//2.4 单链表删除元素
/*
初始条件:顺序线性表L已存在,1≤i≤ListLength(L)
操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1
*/
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;
//将q结点中的数据给e
*e = q->data;
//让系统回收此结点,释放内存;
free(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;
}
//3.1 单链表前插入法
/* 随机产生n个元素值,建立带表头结点的单链线性表L(前插法)*/
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;
//将结点P插入到头结点之后;
(*L)->next = p;
}
}
//3.2 单链表后插入法
/* 随机产生n个元素值,建立带表头结点的单链线性表L(后插法)*/
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 = p;
}
//将尾指针的next = null
r->next = NULL;
}
int main(int argc, const char * argv[]) {
// insert code here...
printf("Hello, World!\n");
Status iStatus;
LinkList L1,L;
struct Node *L2;
ElemType e;
// L1 =(LinkList) malloc(sizeof(Node));
// L2 =(LinkList) malloc(sizeof(Node));
//
// L1->data = 1;
// L2->data = 2;
// printf("L1.data=%d,L2.data=%d\n",L1->data,L2->data);
//2.1 单链表初始化
iStatus = InitList(&L);
printf("L 是否初始化成功?(0:失败,1:成功) %d\n",iStatus);
//2.2 单链表插入数据
for(int j = 1;j<=10;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, 20);
printf("整理创建L的元素(前插法):\n");
ListTraverse(L);
//3.2 后插法整理创建链表L
iStatus = ClearList(&L);
CreateListTail(&L, 20);
printf("整理创建L的元素(后插法):\n");
ListTraverse(L);
}