前言
上一篇《数据结构与算法分析》第二章算法分析笔记、时间复杂度、大O表示法
下一篇 占坑
平常写的文章阅读量很低,有的自认为写的挺好的,如cJSON的delete和free区别一篇,阅读量也平平,但是在上一章的读书笔记中,阅读量突然暴增到两三天几百,这是我没有预料到的,今天(写第三章笔记时)再次看了上一章的文章,觉得写的非常平平,错别字不少,并且很多基础知识点是直接跳过的,并不适合发表出来供大家学习使用,单纯只是自己在做笔记的同时把笔记内容分享出来了而已。
在这一篇章中,我会尽可能把知识点写的更加完善,但是对很多更加浅显的知识,可能是摘抄其他人的,别人可能讲的更好更细。希望观众老爷们在看完本文后有任何想法请在评论区中留下您的足迹。
数据结构的抽象
抽象类似面向对象,将编程与数学等进行抽象,脱离编程本身,以其他形式被观察
举例:
一只猫咪喂食,不同的猫食物分量有区别,因为猫的品种含水量,消化能力也会有细微差别,但是我们给主子买猫粮时,大多的猫粮是不区分猫品种的,所有的猫,相对于一袋猫粮而言,都被抽象为一类,即猫本身。
权威解释
面向对象程序设计(英语:Object-oriented programming,缩写:OOP)是种具有对象概念的编程典范,同时也是一种程序开发的抽象方针。它可能包含数据、特性、代码与方法。对象则指的是类(class)的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象[1][2]。
面向对象程序设计可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反:传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对电脑下达的指令。面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。目前已经被证实的是,面向对象程序设计推广了程序的灵活性和可维护性,并且在大型项目设计中广为应用。此外,支持者声称面向对象程序设计要比以往的做法更加便于学习,因为它能够让人们更简单地设计并维护程序,使得程序更加便于分析、设计、理解。反对者在某些领域对此予以否认。
当我们提到面向对象的时候,它不仅指一种程序设计方法。它更多意义上是一种程序开发方式。在这一方面,我们必须了解更多关于面向对象系统分析和面向对象设计(Object Oriented Design,简称OOD)方面的知识。许多流行的编程语言是面向对象的,它们的风格就是会透由对象来创出实例。
重要的面向对象编程语言包含Common Lisp、Python、C++、Objective-C、Smalltalk、Delphi、Java、Swift、C#、Perl、Ruby、JavaScript 与 PHP等。
作用
- 新的设计模式,更易理解,脱离编程的思想模式
- 解耦合、模块化
表
A1 -> An,表记为A表,下标从1到n,表长度为n,可能会有插入,删除,遍历能操作接口
数组
最简单的表的形式
优点
- 按下标查找时候数组最快,编译时根据下标与数组类型、数组起点直接计算得到指定下标内容的内存地址
- 简单,一行代码就可以申请一个数组 int a[10];
- 除了需要存储的数据本身,几乎没有额外的内存开销
缺点
- 数组容量在申请数组时就确定了,无法后期修改,对于使用数组作为实现方式,同时有增删能力的类,一般使用整个数组搬运的方式实现容量的变化,这样必定是效率低下的
链表
一个节点具有一个指向下一个节点的指针,即通过一个节点,通过它的指针才能找到下一个节点的位置,这被称之为单向链表。如果每个节点同时含有对上一个节点的指针信息,这个链表是双向链表
graph LR
A1-->A2
A2-->null
一个容量为2的单项链表,箭头表示它的下一个节点指针位置
链表的表头
对链表的第一个节点一些操作是特殊的,比如删除,其他节点的删除需要记住并修改前节点,但是表头不需要这些操作,因此,有的习惯喜欢在创建链表时首先给出一个空的头节点,后续的第一个节点才是A1,但这也只是属于习惯问题。
不添加表头的好处在于,如果链表只有一个节点,对头节点的删除将会把整个链表的所有空间清空,但是有空头节点时还是会剩下一个空的头节点。
数组和指针的操作代码
简单示例,展示基本的链表结构体的定义,链表的动态空间申请了记得释放哦
#include <stdio.h>
#include <stdlib.h>
#define ARRAY (1) # 宏定义使用数组,注释该行使用链表
/******************** 数组 **************************/
#ifdef ARRAY
static int a[100];
static int len = 0;
void add(int val)
{
if (len < sizeof(a) / sizeof(int) - 1);
a[len++] = val;
}
int print_all()
{
for (int i = 0; i < len; i++)
printf("array[%d] = %d\n", i, a[i]);
}
/********************* 链表 *************************/
#else
typedef struct s_list // 单项链表
{
struct s_list* next; // 指向下一节点的指针
int val; // 链表节点存储的数据
}s_list;
static s_list mylist =
{
.next = NULL,
.val = 0,
};
static const s_list* mylist_head = &mylist; // 空的头节点,耙
void add(int val)
{
s_list* mylist_cursor = (s_list*)mylist_head; // 游标,记录下当前操作的节点
while(mylist_cursor->next != NULL) // 每次新增都遍历一遍不高校,仅作示范操作
mylist_cursor = mylist_cursor->next; // 遍历到最后一个为空的节点,即链表的尾巴
s_list* mylist_new = malloc(sizeof(s_list)); // 从内存中申请一个新的节点
mylist_new->next = NULL;
mylist_new->val = val;
mylist_cursor->next = mylist_new; // 给大哥把腿接上
}
int print_all()
{
s_list* mylist_cursor = (s_list*)mylist_head; // 游标,记录下当前操作的节点
int i = 0;
mylist_cursor = mylist_cursor->next; // 第一个节点不算,是空的靶节点
while(mylist_cursor != NULL)
{
printf("list[%d] = %d\n", i++, mylist_cursor->val);
mylist_cursor = mylist_cursor->next; // 遍历到最后一个为空的节点,即链表的尾巴
}
}
#endif
int main(int argc, char** argv)
{
for (int i = 0; i < 10; i++)
add(i);
print_all();
return 0;
}
编译
gcc -std=gnu11 -g -O0 cal.c -o cal.out
执行效果
./cal.out
list[0] = 0
list[1] = 1
list[2] = 2
list[3] = 3
list[4] = 4
list[5] = 5
list[6] = 6
list[7] = 7
list[8] = 8
list[9] = 9
[niuwanli@swcentos7 c]$ make
gcc -std=gnu11 -g -O0 cal.c -o cal.out
[niuwanli@swcentos7 c]$ ./cal.out
array[0] = 0
array[1] = 1
array[2] = 2
array[3] = 3
array[4] = 4
array[5] = 5
array[6] = 6
array[7] = 7
array[8] = 8
array[9] = 9
书中的部分
书中将对数据的操作 add print 等这些封装在了.h头文件中作为接口,这样当然是高效的
对于学习笔记而言,能自己实现一个链表,掌握链表的操作即可
对于指针的使用
为什么大多数高级语言开发者指责指针的使用,因为指针是面向内存这种硬件设计的一种数据类型,对于语言的抽象而言,或者对于软件开发者而言,最好是做到与硬件的解耦,一个软件的开发者不应该或者说不需要去知道硬件是怎样工作的。同时,对指针的指向,空指针,野指针,都是需要细心的编程考虑的,另外就是对内存的操作,malloc和free的使用不当容易导致的内存泄漏比较难发现,比较难排查。
c语言怎样对动态内存更加高效的使用?应当把对内存的申请释放封装到一个库中,对库进行足够多的测试验证库的设计正确,在其他地方调用这个库,使用库的接口实现对内存数据的操作
合并多项式
数学的东西比较多,没怎么看,大意是可以使用链表的指针特性将当前结果与之前的结果关联进行存储,不明觉厉
计数排序/桶排序/基数排序
对于1-N^p概率分布的n个数进行排序
比如,在1到10^3-1个数中进行排序,0-999
注: 桶可以是一个链表,即动态容量,可以放置不超过物理限制个数的容器
计数排序
有1000个桶,编号为0-999,需要排序的数字根据桶的编号放置到对应的桶中
放置完成后从编号为0的桶开始,依次将桶中的数据取出
优点
- 简单算不算?甚至可以使用数组存储,速度快
缺点
- 浪费内存,假如只有10个数需要排序,范围为2^64,同样需要2^64容量的内存,对桶的数量依赖上限过高
桶排序
参考
接下来,算法进一步提高,分两步将数据进行排序,目的是减少桶的用量,假如这次只使用100个桶,这样一个桶可以放10个,按照大小顺序,0-9的放第一个桶,10-19放第二个桶。。。100个桶放完后,每个桶里的再单独排序,最后再从第一个桶开始遍历,就可以得到所有数的排序结果。
优点
- 减少桶的数量,并且桶的数量是有限可以控制的,即使再多的数据,依然可以用比较少量的桶先进行第一次的区分
缺点
- 每个桶内的排序依然是一次计数排序或慢速的排序,效率并没有可见的提升,具体的性能提升深度依赖于具体的桶数量与需要排序的数据量
基数排序
这张图片上来自于图书的摘抄截图,这本书里的解释是,0-999一共是1000个数,正好是10的3次方,那么就以底10为桶的数量,进行3次排序即可得到最终的结果。
步骤是这个步骤没错,但是具体什么一个思路得到这样的步骤,我也不清楚,摘抄瞎wiki的解释来看看,wiki使用的是2进制作为底,好处是在计算机中,绝大多数(99.9999999%)是二进制表示的,意味所有的数据都是可以使用这种方式进行排序,通用性强,一次只需要两个桶,排序的次数是数据的存储字长度
基数排序(英语:Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机(Tabulation Machine)上的贡献[1]。
它是这样实现的:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
wiki对时间复杂度的表示
类别 排序算法
数据结构 数组
复杂度
最坏时间复杂度 O(kN)
空间复杂度 O(k+N)
最佳解 Yes
缺点
- 额外的链表存储开销(其实桶排序、计数排序都是这样)
优点
- 排序次数按指数降低,选择合适的桶大小对性能不一定有足够的影响,但是桶的大小是可以预测的,比如,只使用两个桶适合于任何数据类型,而且对效率影响不大
多重表
不同的表头对相同的节点指来指去,此项用的较少,跳过
栈
概念:只从一个端口进和出的表
栈的实现
因为栈也属于一种表,是表的子集,所以可以实现表的方式(数组和链表)均可以实现栈,方法雷同
栈的应用
平衡符号
书中这一段写的已经比较简单,比较详细,直接摘抄
队列
概念:从一个端口进,从另一个端口出的表,依然是一个特殊的表,生活之中的排队,电脑中的打印机打印队列,都是队列的现实实现