数据结构与算法的认知
数据结构
数据结构(data structure)是带有结构特性的数据元素的集合,它研究的是数据的逻辑结构和数据的物理结构以及它们之间的相互关系。
以下是数据结构的大体内容:
举个简单的例子:如果要保存100个数字,按照初始c语言知识我们可以使用数组解决。但是如果你想要在操作中间执行一个插入或者删除,此时数组就难以达成这个目的,而通过数据结构就可以随时进行增删等复杂操作,这是数据结构的初步使用。
算法
很多初学者可能不了解什么叫做算法。
我们可以举个简单的例子:
求1-100相加的和:
我们可以通过for循环进行实现:
#include <stdio.h> int main(){ int a=0; for (int i = 1; i <=100 ; ++i) { a+=i; } printf("%d",a); }
在进行计算的过程中此程序共运行了100次
而算法就是让这一过程减少,我们可以使用高中学习的数列知识进行简化:
高斯公式(数列求和公式)
∑=[(首项+末项)×项数]/2
#include <stdio.h> int main(){ int a; a=((1+100)*100)/2; printf("%d",a); }
这样只需运行一次便可得到答案。
这就是一个算法的实际运用。
接下来是一道经典的算法题,你试试看,尽可能使用更少的次数得到答案。
经典例题:百元买百鸡
100元钱买100只鸡,母鸡每只5元.公鸡每只3元,小鸡3只1元,问共可以买多少只母
鸡、多少只公鸡、多少只小鸡?
提示:利用方程求关系式。(最少4次)
答案:
#include <stdio.h>
int main(){
int x,y,z,n=0;//设x为母鸡,y为公鸡,z为小鸡,n记录运行次数
for ( x = 0; x<=14; x=x+4) {
y=(100-7*x)/4;//利用关系式可以得到以下此式,通过此式可以得到母鸡个数是4的公倍数
z=100-(x+y);
if(x+y+z==100&&5*x+3*y+z/3==100&&(100-7*x)%4==0){
printf("母鸡有%d只\n公鸡有%d只\n小鸡有%d只\n",x,y,z);
}
n++;
}
printf("运行次数为%d次",n);
}
可见,不同的算法,执行的效率也是有很大差别的,这里我们就要提到算法的复杂度了。衡量一个算法的复杂程度需要用到以下两个指标:
时间复杂度T(n):算法程序在执行时消耗的时间长度,一般与输入数据的规模n有关。
空间复杂度S(n):算法程序在执行时占用的存储单元长度,同样与数据的输入规模n有关,大部分情况下,我们都是采取空间换时间的算法。
初始介绍就到这里,感谢大家观看!
线性表
线性表是数据结构最简单的内容,对于数组无法做到的内容就可以使用线性表进行实现,例如前文中提到的删除,增加等操作。
线性表(Linear List)是由同一类型的数据元素构成的有序序列的线性结构。线性表中元素的个数就是线性表的长度,表的起始位置称为表头,表的结束位置称为表尾,当一个线性表中没有元素时,称为空表。
线性表一般需要包含以下功能:
*初始化线性表:将一个线性表进行初始化,得到一个全新的线性表。
*获取指定位置上的元素:直接获取线性表指定位置i上的元素。
*获取元素的位置:获取某个元素在线性表上的位置i。
*插入元素:在指定位置i上插入一个元素。
*删除元素:删除指定位置i上的一个元素。
*获取长度:返回线性表的长度。
后续代码会依次说明。
线性表共有三个小类:顺序表,链表,栈。
顺序表
首先我们先进行顺序表的学习。
数组无法实现这样的高级表结构,那么我就基于数组,对其进行强化,也就是说,我们存放数据还是使用数组,但是我们可以为其编写一些额外的操作来强化为线性表,像这样顺序储存的线性表称为顺序表。
实战操作:
我们首先要定义一个结构体,使他可以储存一些数据:
#include <stdio.h> #include <stdlib.h> typedef int E; struct list { E *arr;//实现顺序表的底层数组 int Long;//数组的容量 int size;//数组已经储存的元素数量 };typedef struct list * node;//由于后续函数要以指针的形式引入所以
这是一个基本的顺序表内容的实现。
然后进行顺序表的初始化,初始化同时赋予容量大小。
_Bool initList(node list) { list->arr= malloc(sizeof (int)*10);//定义数组容量 list->Long=10;//赋予记录名 list->size=0; if (list->arr==NULL)return 0; return 1; }
然后加入最基础的功能:输入
int inputList(node list){ printf("输入你数据的个数:"); int n; scanf("\n%d",&n); while(list->Long<n){ int newLong=list->Long+(list->Long>>1); int * newArr= realloc(list->arr, newLong * sizeof(E)); list->arr=newArr; list->Long=newLong; }//如果客户要输入的数据大于你所设定的初始值,那么使用realloc函数来进行扩充 for (int i = list->size; i < list->Long; ++i) { printf("输入你的数据,第%d个:",i+1); scanf("%d",&list->arr[i]); list->size++;//记住每次操作后size的情况 if(list->size==n)break; } return 1; }
int newLong=list->Long+(list->Long>>1);
'>>'是右移运算符
它将 list->Long
的二进制表示向右移动指定的位数(在这个例子中是 1 位)。右移一位等价于除以 2,但是它是在二进制层面上进行的,不考虑任何除法的舍入或溢出。 位运算通常用于对数据进行底层操作,比如优化存储或进行特定的算法处理。
realloc(list->arr, newLong * sizeof(E));
void *realloc(void *ptr, size_t new_size); realloc函数的原型。
ptr
:指向先前通过malloc
、calloc
或realloc
分配的内存块的指针。如果是NULL
,realloc
的行为类似于malloc
。new_size
:希望重新分配的内存块的新大小,以字节为单位。
realloc
函数的工作原理是尝试在内存中为新的 new_size
大小的内存块找到空间。如果成功,它会将旧内存块的内容复制到新位置(如果有足够的空间),释放旧内存块,并返回指向新内存块的指针。如果无法找到足够大的连续内存空间,realloc
会保持原来的内存块不变,并返回 ptr
。
其次是删除操作:
_Bool insertList(node list){ int index,element; printf("输入你想插入的位数:"); scanf("%d",&index); printf("输入你想插入的数据:"); scanf("%d",&element); if(index<1||index>list->size+1){ printf("输入错误"); return 0; }//框定范围 if(list->size==list->Long){ int newLong=list->Long+(list->Long>>1); int * newArr= realloc(list->arr, newLong * sizeof(E)); list->arr=newArr; list->Long=newLong; }//如果插入后会导致储存空间不够,则继续扩充,原理同上 for (int i = list->size; i >index ; --i) { list->arr[i+1]=list->arr[i]; } list->arr[index-1]=element; list->size++; return 1; }
以下几种操作根据前面的知识可以自行了解。
删除:
_Bool deleteList(node list){ int index; printf("输入你想删除的位数:"); scanf("%d",&index); if(index<1||index>list->size){ printf("输入错误"); return 0; } for (int i = index-1; i < list->size; ++i) { list->arr[i]=list->arr[i+1]; } list->size--; return 1; }
查询数据:
_Bool getList(node list){ printf("输入你想查询的位数: "); int a; scanf("%d",&a); printf("%d\n",list->arr[a-1]); return 1; }
寻找数据:
_Bool findList (node list){ printf("输入你想寻找的数字:"); int n; scanf("%d",&n); for (int i = 0; i <list->size ; ++i) { if (list->arr[i] == n) { printf("yes\n"); return 1; } else printf("No\n"); } return 0; }
输出:
_Bool printList (node list) { for (int i = 0; i <list->size ; ++i) { printf("%d ",list->arr[i]); } printf("\n链表数据大小为%d\n",list->size); printf("链表现在长度为%d\n",list->Long); return 1; }
我们可以专门设立一个函数去使用这些功能:
void qidong(node list){ int a; printf("1.输入数字\n2.插入数字\n3.删除数字\n4.寻找数字\n5.查询位数\n6.输出链表数据\n0.退出程序\n"); scanf("%d",&a); switch (a) { case 1: inputList(list); return qidong(list); case 2: insertList(list); return qidong(list); case 3: deleteList(list); return qidong(list); case 4: findList(list); return qidong(list); case 5: getList(list); return qidong(list); case 6: printList(list); return qidong(list); case 0: return ; } }
最后进行主函数的编程
int main(){ struct list list; if (initList(&list)){ qidong(&list); } printf("谢谢使用"); }
链表(更新中)
链表不同于顺序表,顺序表底层采用数组作为存储容器,需要分配一块连续且完整的内存空间进行使用,而链表则不需要,它通过一个指针来连接各个分散的结点,形成了一个链状的结构,每个结点存放一个元素,以及一个指向下一个结点的指针,通过这样一个一个相连,最后形成了链表。它不需要申请连续的空间,只需要按照顺序连接即可,虽然物理上可能不相邻,但是在逻辑上依然是每个元素相邻存放的,这样的结构叫做链表(单链表)。
链表分为带头结点的链表和不带头结点的链表,戴头结点的链表就是会有一个头结点指向后续的整个链表,但是头结点不存放数据:
而不带头结点的链表就像上面那样,第一个节点就是存放数据的结点,一般设计链表都会采用带头结点的结构,因为操作更加方便。
首先先创建一个链表并进行初始化。
#include <stdio.h> #include <stdlib.h> typedef int E; //这个还是老样子 struct ListNode { E element; //保存当前元素 struct ListNode * next; //指向下一个结点的指针 }; typedef struct ListNode * Node; void initList(Node head){ head->next = NULL; //头结点默认下一个为NULL }int main() { struct ListNode head; //这里创建一个新的头结点,头结点不存放任何元素,只做连接,连接整个链表 initList(&head); //先进行初始化 }
随后便是进行一系列系统的创捷执行。
首先还是输入。
_Bool insertList(Node head){ printf("输入你要输入的个数:"); int index; scanf("%d",&index); if(index<1){ printf("输入错误请重新输入"); return insertList(head); } for (int i = 0; i < index; ++i) { int element; printf("输入数据:"); scanf("%d",&element); Node node = malloc(sizeof (struct ListNode)); if(node == NULL) return 0; //创建一个新的结点,如果内存空间申请失败返回0 node->element = element; //将元素保存到新创建的结点中 node->next = head->next; //先让新插入的节点指向原本位置上的这个结点 head->next = node;} //接着将前驱结点指向新的这个结点 return 1; }
要想插入一个节点,需要使该节点指向下一节点,并使上一节点指向此节点。
删除:
_Bool deleteList(Node head){ int index; printf("输入你想删除的位置"); scanf("%d",&index); if(index < 1) return 0; //大体和上面是一样的 while (--index) { head = head->next; if(head == NULL) return 0; } if(head->next == NULL) return 0; //注意删除的范围,如果前驱结点的下一个已经是NULL了,那么也说明超过了范围 return 1; }
输出:
void printList(Node head){ while (head->next) { head = head->next; printf("%d ", head->element); //因为头结点不存放数据,所以从第二个开始打印 } }
接着便是一些寻找数据,查询数据,数据大小的一系列操作。
int findList(Node head){ int element; printf("输入你想查询的数据"); scanf("%d",&element); head = head->next; //先走到第一个结点 int i = 1; //计数器 while (head) { if(head->element == element) return i; //如果找到,那么就返回i head = head->next; //没找到就继续向后看 i++; //i记住要自增 } return -1; //都已经走到链表尾部了,那么就确实没找到了,返回-1 }E * getList(Node head, int index){ if(index < 1) return NULL; //如果小于0那肯定不合法,返回NULL do { head = head->next; //因为不算头结点,所以使用do-while语句 if(head == NULL) return NULL; //如果已经超出长度那肯定也不行 } while (--index); //到达index就结束 return &head->element; } int sizeList(Node head){ int i = 0; //从0开始 while (head->next) { //如果下一个为NULL那就停止 head = head->next; i++; //每向后找一个就+1 } return i; }
这样我们链表的操作就完成了。