数据结构与算法
播放链接: https://www.bilibili.com/video/BV1K7411q7he?p=2.
数据结构定义
数据结构就是帮我们解决如何组织和存储数据的方式。
数据结构主要是研究非数值计算问题的程序中操作对象以及他们之间的关系,不是研究复杂的算法的。
算法的基本概念
算法是特定问题求解步骤的描述
对于算法而言,最重要的是思想。
算法与数据结构的关系
算法是为了解决实际问题而设计的。
数据结构是算法需要处理的问题载体。
一个好的算法是需要依赖于数据结构的。
算法效率的度量
原文链接: https://blog.csdn.net/weixin_40136018/article/details/81365198.
时间复杂度的分类
1、分析算法时,存在几种可能的考虑:
算法完成工作最少需要多少基本操作,即最优时间复杂度
算法完成工作最多需要多少基本操作,即最坏时间复杂度
算法完成工作平均需要多少基本操作,即平均时间复杂度
对于最优时间复杂度,其价值不大,因为它没有提供什么有用信息,其反映的只是最乐观最理想的情况,没有参考价值。
对于平均时间复杂度,是对算法的一个全面评价,因此它完整全面的反映了这个算法的性质。但另一方面,这种衡量并没有保证,不是每个计算都能在这个基本操作内完成。而且,对于平均情况的计算,也会因为应用算法的实例分布可能并不均匀而难以计算。
对于最坏时间复杂度,提供了一种保证,表明算法在此种程度的基本操作中一定能完成工作。
因此,我们主要关注算法的最坏情况,亦即最坏时间复杂度。
利用大O表示法计算算法的时间复杂度
常见的时间复杂度
注意,log2n 换算成 logn
分析如下:
根据换底公式,logan 可换算成 logcn / logca。
再根据计算时间复杂度有一个原则:“如果算法复杂度的最高次项的乘数不是1,直接舍去。”,因此,又可换算成 logcn,又c为任意常数,直接取为1。所以最后换算为logn。
常见的时间复杂度之间的关系
所消耗的时间从小到大
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
时间复杂度的几条基本计算规则
1、判断一个算法的效率时,往往只需要关注操作数量的最高次项,其他次要项和常数项可以忽略。
2、只有常数项,记作1。
3、log2n 换算成 logn
4、如果算法复杂度的最高次项的乘数不是1,直接舍去。
大O表示法练习题
O(3) ==》O(1)
O(n)
int count = 1,(count * 2 * 2 … *2中的count直接带入1)
假设
第 1 次 :21 < n
第 2 次 :22 < n
第 3 次 :23 < n
…
当
第 x 次 :2x > n
则 x = log2n
因此,O( log2n) ==》O( logn)
O( n2)
i = 0,for (j=i; j<n; j++) 执行 n 次
i = 1,for (j=i; j<n; j++) 执行 n - 1次
i = 2,for (j=i; j<n; j++) 执行 n - 2 次
…
i = n,for (j=i; j<n; j++) 执行 0 次
因此,
n + (n-1) + … + 1 + 0 = n (n + 1) / 2
从而时间复杂度为 O( n2)
线性表
数据结构包括数据元素的逻辑结构、存储结构和运算。
线性表是一种逻辑结构。而非存储结构。
线性表的定义
线性表的特点
线性表的性质
线性表的九种基本操作
线性表的两种存储方式 — 顺序存储和链式存储
顺序存储—顺序表
线性表的顺序存储又称顺序表
顺序表的定义
用一段地址连续的存储单元依次存储线性表的数据元素。
如何用程序设计语言来描述顺序表?
顺序表可以用数组来实现,而描述数组有两种方式,从而可确定两种分配方式,也就可以有两种方式来描述顺序表。即数组动态分配和数组静态分配。
对于数组动态分配,只要根据第一个数据元素的地址和类型,就可以计算出任意数据元素的位置。也就是说只要定义了第一个数据元素的指针,就可以描述整个顺序表。而对于ElemType *data;仅仅只是一个地址,而没有确切的空间,因此在使用时还需要进行动态申请空间。
使用数组来描述顺序表时需要注意:
顺序表的标号是从1开始的,而数组下标则是从0开始的。
线性表顺序存储案例
动态数组
拿到一块数据空间的首地址,就相当于拿到这块数据空间中的数据。
My_Dynamic_Array.c
#include <stdio.h>
#include <stdlib.h> //system("pause");需要用到
#include <string.h>
#include "DynamicArray.h"
void test01()
{
//初始化一个动态数组
Dynamic_Array* myArray = Init_Dynamic_Array();
//打印容量
printf("数组容量:%d\n", GetCapacity_Array(myArray));
//打印数组大小
printf("数组大小:%d\n", GetSize_Array(myArray));
//插入元素
for (int i = 0; i < 30; i++)
{
PushBack_Array(myArray, i);
}
//删除--根据指定位置删除数据
RemoveByPos_Array(myArray, 5);
//打印容量
printf("数组容量:%d\n", GetCapacity_Array(myArray));
//打印数组大小
printf("数组大小:%d\n", GetSize_Array(myArray));
//打印
Print_Array(myArray);
//释放动态数组的内存
FreeSpace_Array(myArray);
}
int main(void)
{
test01();
system("pause");
return 0;
}
Dynamic_Array.h
#ifndef DYNAMIC_ARRAY_H
#define DYNAMIC_ARRAY_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//动态增长内存,策略是将存放数据的内存放到堆上。
//现有一个动态数组,如果只能存放5个元素,现在想存放6个,就需要执行“申请内存 -> 拷贝数据 -> 释放内存”
//那现在我又想存放7个元素,那就必须再次执行“申请内存 -> 拷贝数据 -> 释放内存”这三步,因此效率很低。
//容量capacity,表示我的这块内存空间一共可以存放多少元素。
//size概念,记录当前数组中具体的元素个数。
//动态数组结构体定义
typedef struct DYNAMICARRAY
{
int* pAddr;//存放数据的地址
int size;//当前有多少个元素
int capacity;//容量,我容器当前最大能容纳多少元素
}Dynamic_Array;
//写一系列对DYNAMICARRAY结构体操作的函数
//写API时,要通过与其他函数通过后缀进行区分,比如这里都是以Array结尾
//初始化动态数组
Dynamic_Array* Init_Dynamic_Array();
//插入--因为默认是从尾部插入,所以不需要指定位置,因此也就不需要传指定位置的参数。
void PushBack_Array(Dynamic_Array* arr,int value);
//删除--根据指定位置删除数据
void RemoveByPos_Array(Dynamic_Array* arr, int pos);
//删除--删除指定数据
void RemoveByValue_Array(Dynamic_Array* arr, int value);
//查找
int Find_Array(Dynamic_Array* arr, int value);
//打印
void Print_Array(Dynamic_Array* arr);
//清空数组
void Clear_Array(Dynamic_Array* arr);
//获得动态数组容量
int GetCapacity_Array(Dynamic_Array* arr);
//获得当前动态数组元素的个数
int GetSize_Array(Dynamic_Array* arr);
//根据位置获得某个位置元素
int GetByPos_Array(Dynamic_Array* arr, int pos);
//释放动态数组的内存
void FreeSpace_Array(Dynamic_Array* arr);
#endif // !DYDAMIC_ARRAY
Dynamic_Array.c
#include "DynamicArray.h"
Dynamic_Array* Init_Dynamic_Array() {
//申请内存
Dynamic_Array* myArray = (Dynamic_Array*)malloc(sizeof(Dynamic_Array));
if (myArray == NULL)
{
return -1;
}
//初始化
myArray->size = 0;
myArray->capacity = 20;
myArray->pAddr = (int* )malloc(sizeof(int)*(myArray->capacity));
//等价于定义了int myArray->pAddr[myArray->capacity];
if (myArray->pAddr == NULL)
{
return -2;
}
return myArray;
}
//插入--因为默认是从尾部插入,所以不需要指定位置,因此也就不需要传指定位置的参数。
void PushBack_Array(Dynamic_Array* arr, int value)
{
if (arr == NULL)
{
return NULL;
}
//判断空间是否足够
if (arr->size == arr->capacity)
{
//第一步:申请一块更多的内存空间,这里设定新空间是旧空间的两倍
int* newSpace = (int* )malloc(sizeof(int) * arr->capacity * 2);
if (newSpace == NULL)
{
return -2;
}
//第二步:拷贝数据到新的空间
//一个字节一个字节的拷贝
memcpy(newSpace,arr->pAddr,arr->capacity*sizeof(int));
//第三步:释放旧空间的内存
free(arr->pAddr);
//第四步:更新容量
arr->capacity = arr->capacity * 2;
arr->pAddr = newSpace;
}
//插入新元素--数组从零开始,所以不需要加1。
arr->pAddr[arr->size] = value;
arr->size++;
}
//删除--根据指定位置删除数据
void RemoveByPos_Array(Dynamic_Array* arr, int pos)
{
if (arr == NULL)
{
return NULL;
}
//判断位置是否有效
if (pos < 0 || pos >= arr->size)
{
printf("位置错误.\n");
return;
}
//删除元素
for (int i = pos; i < arr->size - 1; i++)
{
arr->pAddr[i] = arr->pAddr[i+1];
}
arr->size--;
}
//删除--删除指定数据
void RemoveByValue_Array(Dynamic_Array* arr, int value)
{
//找到值的位置,遍历数组
//只删除第一次寻找到的值,如果顺序表中有多个相同值的元素,只需多次调用即可。
int pos = Find_Array(arr, value);
//根据位置删除数据
RemoveByPos_Array(arr, pos);
}
//查找
int Find_Array(Dynamic_Array* arr, int value)
{
if (arr == NULL)
{
return NULL;
}
//找到值的位置,遍历数组
int pos = -1;
for (int i = 0; i < arr->size; i++)
{
if (arr->pAddr[i] == value)
{
pos = i;
break;
}
}
return pos;
}
//打印
void Print_Array(Dynamic_Array* arr) {
if (arr == NULL)
{
return NULL;
}
for (int i = 0; i < arr->size; i++)
{
printf("%d ", arr->pAddr[i]);
}
printf("\n");
}
//清空数组
void Clear_Array(Dynamic_Array* arr) {
if (arr == NULL)
{
return NULL;
}
//我们往pAddr所指向的空间插入值是根据size插入的,以前的值直接覆盖掉。
arr->size = 0;
}
//获得动态数组容量
int GetCapacity_Array(Dynamic_Array* arr) {
if (arr == NULL)
{
return NULL;
}
return arr->capacity;
}
//获得当前动态数组元素的个数
int GetSize_Array(Dynamic_Array* arr) {
if (arr == NULL)
{
return NULL;
}
return arr->size;
}
//根据位置获得某个位置元素
int GetByPos_Array(Dynamic_Array* arr, int pos) {
if (arr == NULL)
{
return NULL;
}
return arr->pAddr[pos];
}
//释放动态数组的内存
void FreeSpace_Array(Dynamic_Array* arr) {
if (arr == NULL)
{
return NULL;
}
if (arr->pAddr != NULL)
{
arr->pAddr == NULL;
}
if (arr != NULL)
{
free(arr);
arr = NULL;
}
}
动态数组如何进行删除
链式存储—链表
针对链表操作的API的框架建立
新建SListAPI.h文件
#ifndef SLISTAPI_H
#define SLISTAPI_H
#include <stdio.h>
#include <stdlib.h> //system("pause");需要用到
#include <string.h>
//定义链表结点
typedef struct SListNode
{
void* data;
struct SListNode* next;
}SListNode;
//定义链表结构体
typedef struct SLinkList
{
SListNode* head;
int size;
//需要容量的变量吗?
//根据所需去申请内存,不需要提前分配好一块连续的内存。
}SLinkList;
//打印的函数指针
typedef void(*PRINTLINKNODE)(void*);
//1、初始化链表
SLinkList* Init_SLinkList();
//2、在指定位置插入结点
void Insert_SLinkList(SLinkList* list,int pos, void* data);
//3、删除指定位置的结点
void RemoveByPos_SLinkList(SLinkList* list,int pos);
//4、获得链表的长度
int Size_SLinkList(SLinkList* list);
//5、查找并返回链表中数据为data的首个结点的下标。
int Find_SLinkList(SLinkList* list,void* data);
//6、打印链表结点中的数据
void Print_SLinkList(SLinkList* list, PRINTLINKNODE print);
//7、返回第一个有效数据结点
void* Front_SLinkList(SLinkList* list);
//8、释放链表内存
void FreeSpace_SLinkList(SLinkList* list);
#endif
针对链表操作的API的框架实现
定义链表结点
无类型指针可以指向任何类型的数据。
typedef struct SListNode
{
void* data;
struct SListNode* next;
}SListNode;
1、初始化链表
我们只需要维护这个链表结构体,对这个结构体进行操作即可。
//初始化链表
//我们新建了一个链表结构体,只需要维护这个链表结构体,对这个结构体进行操作即可。
SLinkList* Init_SLinkList()
{
SLinkList* list = (SLinkList*)malloc(sizeof(SLinkList));
//初始化链表
//1、首先需要给它一个头结点,头结点不保存数据信息
list->head = (SListNode *)malloc(sizeof(SListNode));
list->head->data = NULL;
list->head->next = NULL;
//2、size初始值为0
list->size = 0;
return list;
}
2、在链表的指定位置插入结点
友好的处理,pos越界或者小于0,直接插入到尾部。
链表的位置从开始。
注意:找节点怎么找?
插入新结点后,size别忘了加1
画图分析:
//在指定位置插入结点
void Insert_SLinkList(SLinkList* list,int pos, void* data)
{
if (list == NULL)
{
return;
}
if (data == NULL)
{
return;
}
//友好的处理,pos越界或者小于0,直接插入到尾部
if (pos <0|| pos >list->size)
{
pos = list->size;
}
//创建新的结点
SListNode* pNewNode = (SListNode* )malloc(sizeof(SListNode));
//给新结点初始化
pNewNode->data = data;
pNewNode->next = NULL;
//找结点
//定义辅助指针变量
SListNode* pCurPos = list->head;
for(int i=0;i<pos;i++)
{
pCurPos = pCurPos->next;
}
//定义两个辅助变量并设置初始值
SListNode* pCurNode = pCurPos;
//将新结点插入链表,顺序不能乱
pNewNode->next = pCurNode->next;
pCurNode->next = pNewNode ;
list->size++;
}
3、在链表的指定位置删除结点
pos越界(pos >= size,因为等于size表示指向NULL)或者小于0,直接返回。
删除结点后,size别忘了减1
画图分析:
//删除指定位置的结点
void RemoveByPos_SLinkList(SLinkList* list,int pos)
{
if (list == NULL)
{
return;
}
if (pos <=0 || pos >=list->size)
{
return;
}
//定位到要删除结点的前一个结点
//定义辅助指针变量
SListNode* pPrePos = list->head;
for (int i = 1; i < pos; i++)
{
pPrePos = pPrePos->next;
}
//定义两个辅助变量并设置初始值
SListNode* pPreNode = pPrePos;
SListNode* pDelNode = pPreNode->next;
//删除结点
pPreNode->next = pDelNode->next;
//释放被删除结点的内存
free(pDelNode);
list->size--;
}
4、获得链表的长度(有效数据的个数)
//获得链表的长度(有效数据的个数)
int Size_SLinkList(SLinkList* list)
{
if (list == NULL)
{
return;
}
return list->size;
}
5、查找并返回链表中数据为data的首个结点的下标。
链表中有效数据的结点是head结点的下一个结点
//查找并返回链表中数据为data的首个结点的下标。
int Find_SLinkList(SLinkList* list, void* data)
{
if (list == NULL)
{
return;
}
if (data == NULL)
{
return;
}
SListNode* pCurNode = list->head->next;
int pos = 1;
//遍历查找
while (pCurNode != NULL)
{
if (pCurNode->data == data)
{
break;
}
pos++;
pCurNode = pCurNode->next;
}
return pos;
}
6、打印链表结点中的数据
//打印链表结点中的数据
void Print_SLinkList(SLinkList* list, PRINTLINKNODE print)
{
if (list == NULL)
{
return;
}
printf("head -> ");
//辅助指针变量
SListNode* pCurNode = list->head->next;
while (pCurNode!=NULL)
{
print(pCurNode->data);
pCurNode = pCurNode->next;
}
printf("NULL.\n");
}
7、返回第一个有效数据结点中的数据
//返回第一个有效数据结点中的数据
void* Front_SLinkList(SLinkList* list)
{
return list->head->next->data;
}
8、释放链表内存
先释放各个结点的内存,再释放整个链表的内存。
//释放链表内存
void FreeSpace_SLinkList(SLinkList* list)
{
if (list == NULL)
{
return;
}
//定义辅助指针结点变量来标识每次要释放的结点
SListNode* pCurNode = list->head;
//缓存下一个要释放的结点
SListNode* pNextNode = NULL;
//记录释放结点次数
int i = 0;
while (pCurNode != NULL)
{
i++;
//缓存下一个要释放的结点
pNextNode = pCurNode->next;
free(pCurNode);
pCurNode = pNextNode;
}
printf("释放结点个数为:%d.\n", i);
//释放链表内存
list->size = 0;
free(list);
}
针对链表操作的API的框架测试
#include <stdio.h>
#include <stdlib.h> //system("pause");需要用到
#include <string.h>
#include "SListAPI.h"
//自定义数据类型
typedef struct PERSON
{
char name[64];
int age;
int score;
}PERSON;
//打印函数
void MyPrint(void* data)
{
PERSON* p = (PERSON*)data;
printf("Name:%s, Age = %3d , score = %3d. ->\n", p->name, p->age, p->score);
}
int main(void)
{
//1、创建链表
SLinkList* mylist = Init_SLinkList();
//自定义数据类型
//静态数组
PERSON person[5] = {
{ "aaa",18,100},
{ "bbb",19,99 },
{ "ccc",20,87 },
{ "ddd",21,59 },
{ "eee",22,68 }
};
//2、向链表中插入数据
for (int i = 0; i < 5; i++)
{
Insert_SLinkList(mylist, i, &person[i]);
}
Print_SLinkList(mylist, MyPrint);
//3、删除指定位置的结点
RemoveByPos_SLinkList(mylist, 1);
printf("删除后:\n");
Print_SLinkList(mylist, MyPrint);
//4、获得链表的长度(有效数据的个数)
printf("链表的长度为:%d.\n", Size_SLinkList(mylist));
//5、查找并返回链表中数据为data的首个结点的下标。
//不能这么查找,查不到。
/*
PERSON a = { "aaa",18,100 };
或
PERSON a = person[1];
int pos = Find_SLinkList(mylist, &a);
printf("pos为:%d.\n", pos);
*/
//只能这样查
//int pos = Find_SLinkList(mylist, &person[1]);
//printf("pos为:%d.\n", pos);
//或
PERSON* a = &person[1];
int pos = Find_SLinkList(mylist, a);
printf("pos为:%d.\n", pos);
//7、返回第一个有效数据结点中的数据
PERSON* ret = (PERSON*) Front_SLinkList(mylist);
printf("第一个有效数据结点中的数据为:Name:%s, Age = %3d , score = %3d.\n", ret->name, ret->age, ret->score);
//8、释放链表内存
FreeSpace_SLinkList(mylist);
mylist = NULL;
printf("\n");
system("pause");
return 0;
}