数据结构成神篇1-初学数据结构

简介: 今天我们开始数据结构的学习,当然,这个有些概念是十分抽象的,只看文章是不一定能懂的,或者说会耗费不少的时间。所以我会持续在B站上面更新讲解视频,都是自己的一些理解和想法。会拿出来和大家一起分享,都是免费的。原创不易,希望大家可以三连支持一下,也希望能给大家带来进步。

今天我们开始数据结构的学习,当然,这个有些概念是十分抽象的,只看文章是不一定能懂的,或者说会耗费不少的时间。所以我会持续在B站上面更新讲解视频,都是自己的一些理解和想法。会拿出来和大家一起分享,都是免费的。原创不易,希望大家可以三连支持一下,也希望能给大家带来进步。


博客地址:

喜欢吃豆的博客_CSDN博客-C++笔记,C语言笔记,C/C++课外闲谈领域博主

https://blog.csdn.net/m0_63309778?spm=1000.2115.3001.5343

喜欢吃豆 的个人主页 - 动态 - 掘金 (juejin.cn)

https://juejin.cn/user/251108556018989

仓库地址:

喜欢吃豆 (i-like-beans) - Gitee.com

https://gitee.com/i-like-beans

B站视频地址:

忘了吃豆的个人空间_哔哩哔哩_bilibili

https://space.bilibili.com/1032401779?spm_id_from=333.1007.0.0


谢谢大家的支持了。


大家学习这个数据结构之前需要有一本数据结构(第二版C语言)书是最好的。


一,引子


首先,我们先考虑一下,什么是数据结构?我们既然要学习数据结构,那我们就应该知道,数据结构是什么呢?


其实这个问题至今仍然没有一个官方的解答,但是有很多大牛都给出了自己的见解。


ee107bb2c7834e4dbab4e13da72f5b6e.png


有多少人可以看懂?在这里,所有的老师都会选择使用例子来让大家更好的理解。


一,例一:


书上给的例子是,如何在书架上面摆放图书?


大家可能会有各种各样的想法,因为很少有事情是绝对的,很多事情都有多种解决方法的。


首先,我们来看一看这个问题,这个问题从本质上来说就是不科学的。


准确来说是不够严谨的,我并没有规定书的规模,还有书架的款式。


那如果我们不那么严谨,我们来考虑以下,有什么样的解决方法?


1,随便放,见缝插针。但是找书的时候会很痛苦,你甚至都不能确定你是否有这本书


2,按书名的拼音的首字符来排序,但是如果这样的话我如果要插入新书,就会对后面所有的书本进行移动


3,把书架划分为几块区域,每块区域指定摆放某种类别的图书,在每种类别内,按照书名的拼音字母顺序排放。但是我们并不知道每一类书有多少本,需要多大的一个区域来存放这些书,会造成空间的一个浪费。


所以我们可以知道解决问题方法的效率,跟数据的组织方式有关。


二,例二:


写程序实现一个函数PrintN,使得传入一个正整数为N的参数后,能顺序打印从1~N的所有整数。


#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//打印从1~N的全部整数
void PrintN(int N) {
  int i;
  for (i = 1; i <= N; i++) {
    printf("%d\n", i);
  }
}
//递归实现
void PrintN(int N) {
  if (N > 0) {
    PrintN(N - 1);
    printf("%d\n", N);
  }
}
int main() {
  int N;
  scanf("%d", &N);
  PrintN(N);
  return 0;
}


这里我给出了 两个版本,一个是用循环语句实现,还有一个是以递归的形式实现,似乎两种方法都可以完成任务。


我们在测试时可以发现,当N充分大的时候,递归实现居然拒绝工作了,这是为什么呢?


我们就要来考虑递归的内部实现了,我们知道,调用函数时,会开辟一块空间让他存储变量,但是递归中有N个函数的调用,它的函数调用不执行完,之前的函数调用也不会被释放,如果调用的足够多,占有的空间也会足够多,直到将内存空间全部占用后,有可能函数的调用还是没有结束,那么就可能会导致程序崩溃,因为内存中没有足够的空间提供让程序进行计算。


所以我们知道,解决问题方法的效率,跟空间利用效率有关。


三,例三:


10e341d20d6b424e96b148b076f21a9f.jpg


最直接的方法就是暴力解法,根据多项式的标准表达式通过循环累计求和来实现这个函数。


#include<stdio.h>
#include<math.h>
//暴力
double f(int n, double a[], double x) {
  int i;
  double p = a[0];
  for (i = 1; i <= n; i++) {
    p += a[i] * pow(x, i);
  }
  return p;
}
//秦九韶解法
double f(int n, double a[], double x) {
  int i;
  double p = a[n];
  for (i = n; i > 0; i++) {
    p = a[i - 1] + x * p;
  }
  return p;
}

729178bf99454edaabcffc05cf1dc255.jpg


这就是秦九韶公式。


我们看上面的两个函数看起来都差不多的样子,但实际上呢?


这里就需要我们学习一个clock工具了。


#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<time.h>
clock_t start, stop;//clock_t是clock函数返回值的类型
double duration;//记录被测函数运行时间,以秒为单位
int main() {
  //不在测试范围内的准备工作写在clock函数调用之前
  start = clock();//开始计时
  MyFunction();//这里写入想测的函数
  stop = clock();//停止计时
  duration = ((double)(stop - start))/CLK_TCK;//计算运行时间
  //CLK_TCK是机器时钟每秒所走的时钟打点数
  //在某些IDE下也可能叫做CLOCKS_PER_SEC
  //其他不在测试范围的处理写在后面。
  return 0;
}


所以我们知道,解决问题方法的效率,跟算法的巧妙程度有关。


通过上面的三个例子,我们知道了,解决问题方法的效率与其相关的因素,那这些因素的定义与什么有关呢?、


好,我们现在正式开始数据结构的一个整体大概的介绍。


什么是数据结构?


1,数据对象在计算机中的组织方式,


1)逻辑结构(线性or非线性)


2)物理存储结构(数组还是链表)


2,数据对象必定与一系列加在其上的操作相关联


3,完成这些操作所用的方法就是算法


我们讨论数据对象的各种不同的组织方式,是为了得到处理这些数据对象的最高效的算法(依需求而定)。所以我们在讨论数据结构这个概念时,关心的不仅仅是数据对象本身以及他们在计算机中的组织方式,还要关心与他们相关联的一个操作集,以及实现这些操作的最高效的算法。


四,抽象数据类型


抽象数据类型是一种对“数据类型”的描述,这种描述是抽象的


数据类型:数据对象集,与数据相关的操作集


抽象:我们描述数据类型是不依赖于具体实现的,即数据对象集和操作集的描述与存放数据的机器无关,与数据存储的物理结构无关,与实现操作的算法和编程语言无关。


总的来说,只描述数据对象集和相关操作集“是什么”并不涉及“如何做到”的问题。


我们还要重点讲一讲抽象,抽象是计算机求解问题的基本方式和重要手段,它使得一种设计可以应用于多种场景。而且可以通过抽象屏蔽掉底层的细节,使得设计更加简单,理解更加方便。


二,算法


算法的设计是一门艺术,解决同一个问题,一般有多种算法,但漂亮的算法与其他算法相比往往有天壤之别。


1,什么是算法?


1)一个有限的指令集


2)接受一些输入(也可以不接受)


3)产出输出(不然没有意义)


4)一定在有限步骤之后终止


5)每一条指令必须有充分明确的目标,不可以有歧义。必须在计算机能处理的范围之内。且描述应不依赖于任何一种计算机语言以及具体实现手段。


2.算法复杂度


什么是好的算法?


比较算法优劣的指标:


1)空间复杂度S(n):根据算法写成的程序在执行时占用存储单元的长度


若过高,会导致内存超出限制,造成程序的非正常中断。


2)时间复杂度T(n):根据算法写成的程序在执行时耗费的时间的长度。


3,在分析一般算法的效率时,我们经常会关注下面的两种复杂度:


ef415ac5a424441ab98bccc6a9f5b510.jpg


对最坏情况复杂度的分析会更容易,因为很多时候平均并不是一件简单的事。


4,渐近表示法:


当n足够大的时候,一些常数或者说一些低阶次的变量的影响就不会那么大了,所以我们一般用渐近表示法。


a4f73f61edba4e0baf0da812e4e16ace.jpg


可能大家对不同的空间复杂度和时间复杂度没有一个很明确的认识,给大家看看这三个表就知道了。


b42246ffeedf4a55a49716345374cb8c.jpg

fab7372304d040aaaff3246885af6ef1.jpg

d4285d1d1af64db58ee6e733f6981299.jpg


告诉大家一些对给定算法做渐进分析时的小窍门:


18c09d27b1244ccab8bd6930808ad9f3.jpg


三,应用实例:最大子列和问题


我们这里将会让大家更加清楚的认识到算法效率的差别,加深大家对算法的理解


64c7f19d680f47e68b3402d751c59ab0.jpg


1,穷举所有子列和,从中找出最大值


int MaxSubseqsuml(int list[],int N) {
  int i, j, k;
  int ThisSum, MaxSum = 0;
  for (i = 0; i < N; i++) {//i是子列左端的位置
    for (j = i; j < N; j++) {//j是子列右端的位置
      ThisSum = 0;//ThisSum是i到j的子列和
      for (k = i; k <= j; k++) {
        ThisSum += list[k];
      }
      if (ThisSum > MaxSum)
        MaxSum = ThisSum;
    }
  }
  return MaxSum;
}


2,少用一个for循环


int MaxSubseqsuml(int list[], int N) {
  int i, j, k;
  int ThisSum, MaxSum = 0;
  for (i = 0; i < N; i++) {//i是子列左端的位置
    for (j = i; j < N; j++) {//j是子列右端的位置
      ThisSum += list[j];
      if (ThisSum > MaxSum)
        MaxSum = ThisSum;
    }
  }
  return MaxSum;
}


3,分而治之


顾名思义,分而治之的基本思路就是将原问题拆分为若干个小问题,分别解决后在将结果和而治之


,用递归实现十分方便。


#define _CRT_SECURE_NO_WARNINGS 1
int Max3(int a, int b, int c) {
  return a > b ? a > c ? a : c : b > c ? b : c;
}
int DivideAndConquer(int List[], int left, int right) {
  //分治法求List[left]到List[right]的最小子列和
  int MaxLeftSum, MaxRightSum;//存放左右子问题的解
  int MaxLeftBorderSum, MaxRightBoderSum;//存放跨分界线的结果
  int LeftBorderSum, RightBorderSum;
  int center, i;
  if (left == right) {//递归的结束条件,子列只有一个数字
    if (List[left] > 0)
      return List[left];
    else return 0;
  }
//下面是分的过程
  //找到中分点
  center = (left + right) / 2;
  //递归求两边子列的最大和 
  MaxLeftSum = DivideAndConquer(List, left, center);
  MaxRightBoderSum = DivideAndConquer(List, center + 1, right);
  //下面求跨分界线最大子列和
  MaxLeftBorderSum = 0;
  LeftBorderSum = 0;
  for (i = center; i >= left; i--) {
    LeftBorderSum += List[i];
    if (LeftBorderSum > MaxLeftBorderSum)
      MaxLeftBorderSum = LeftBorderSum;
  }
  //左边扫描结束
  MaxRightBoderSum = 0;
  RightBorderSum = 0;
  for (i = center + 1; i <= right; i++) {
    RightBorderSum += List[i];
    if (RightBorderSum > MaxRightBoderSum)
      MaxRightBoderSum = RightBorderSum;
  }
  //右边扫描结束
  //最后进行比较
  return Max3(MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBoderSum);
}
int MaxSubseqsum3(int List[],int N) {
  return DivideAndConquer(List, 0, N - 1);
}


4,在线处理


“在线”的意思是指每输入一个数据就进行即时处理,得到的结果是对于当前已经读入的所有数据都成立的解,即在任何地方中止输入,算法都能正确给出正确的解。


int MaxSubseqSum4(int List[], int N) {
  int i;
  int ThisSum, MaxSum;
  ThisSum = MaxSum = 0;
  for (i = 0; i < N; i++)
  {
    ThisSum += List[i];
    if (ThisSum > MaxSum)
      MaxSum = ThisSum;
    else if (ThisSum < 0)
      ThisSum = 0;
  }
  return MaxSum;
}


这种算法并不要求存储序列中的数据,只需要一个一个读入,同时一个一个处理即可,处理过的数据没必要存储,可以说是最快的算法了。

目录
相关文章
|
存储
数据结构成神篇4:树(下)
迭代函数:由于非递归函数的执行效率高,可将“尾递归” 函数改为迭代函数
170 0
数据结构成神篇4:树(下)
|
存储 算法
数据结构成神篇3:树(上)
所谓线性,指的是任何元素至多有一个前驱或后驱元素。一般情况下,不管是用数组还是用链表实现,对线性表的大多数的操作总避免不了线性的时间复杂度,这是由于其逻辑结构是线性这一本质所决定的。
90 0
数据结构成神篇3:树(上)
|
存储 算法
数据结构成神篇2:线性结构
前面我们举过一个一元多项式的例子,我们继续用那个例子来说,前面我们讲的是计算,那我们这节课来讲一讲存储。我们该如何将一个一元多项式的表达式存储到电脑里面呢?
83 0
数据结构成神篇2:线性结构
|
1月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
178 9
|
1月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
32 1
|
23天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
44 5
|
1月前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
1月前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
|
1月前
|
存储
系统调用处理程序在内核栈中保存了哪些上下文信息?
【10月更文挑战第29天】系统调用处理程序在内核栈中保存的这些上下文信息对于保证系统调用的正确执行和用户程序的正常恢复至关重要。通过准确地保存和恢复这些信息,操作系统能够实现用户模式和内核模式之间的无缝切换,为用户程序提供稳定、可靠的系统服务。
51 4
|
2月前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
47 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器