该系列文章系个人读书笔记及总结性内容,任何组织和个人不得转载进行商业活动!
算法精讲-1_1:介绍性内容和指针操作
第一部分:介绍性内容,指针操作、递归、算法分析;
第二部分:引用基本的数据结构,链表、栈、队列、集合、哈希表、树、堆、优先级队列以及图;
第三部分:引入解决问题的常用算法,排序、查找、数值分析、数据压缩、数据加密、图论以及几何计算;
第一部分 预备知识
第一章:数据结构和算法概念,以及使用原因
第二章:若干指针主题;
第三章:递归;
第四章:如何评估和分析算法
第一章 概述
数据结构简介:
数据有各种形式和大小,但通常他们可以以相同的方式来组织;在计算机科学领域中,一些常用来组织数据的方式有:链表、栈、队列、集合、哈希表、树、堆、优先级队列和图;
使用数据结构的三个原因是,效率、抽象和重用性;
效率
数据结构组织数据的方式使得算法变得更加高效;比如,通过哈希表或二叉树可以显著地提高检索的速度;
抽象
数据结构使我们以一种更加容易理解的方式去看待数据,为解决问题提供了一层抽象概念;
重用性
数据是可重用的,因为它们应该是模块化且与上下文无关的;每种数据结构都有各自指定的接口,且只能通过定义接口的操作来访问数据;数据结构能在任意环境或上下文中应用于任意一种类型的数据之上;
给定一个数据结构,通常会想到特定的行为或操作,数据结构加上这些基本操作就称为抽象数据类型(ADT);一个抽象数据类型的操作就组成它的公共接口;
算法简介:
算法是定义良好的用来解决问题的步骤;和数据结构一样,使用算法也有3个原因:效率、抽象和重用性;
效率
在排序算法中,比较明显;
抽象
在解决问题时,算法能够提供一定程度的抽象,因为很多看似复杂的问题都可以用已存在的著名算法来简化;
重用性
算法在很多不同场景下能够得到重用;
算法设计的一般方法:
通常按照算法采用的方法和思路来进行分类;当然,有些算法有悖于分类法则,而另一些则是多种方法相结合的产物;
随机法
随机法依赖于随机数的统计特性;一个应用随机法的例子是快速排序;(快速排序的平均性能很不错,因为随机数的正态分布使得中间值的划分结果相对平衡)
分治法
分治法包含三个步骤:分解、求解和合并;
分解将数据分解为更小、更易管理的部分;求解,对每个分解出的部分进行处理;合并时,将每个部分处理的结果进行审核;一个分治法的例子是归并排序;
在所有分治法算法中都有相同的三个步骤;归并排序能以以下的方式来进行描述:
首先,在分解阶段将数据划分成两份;接下来,按照递归的方式对分解出的两部分应用归并排序;最后,在合并阶段将两个部分合并成一个排好序的集合;
动态规划
类似分治法,都是将较大问题分解为子问题最后再将结果合并;只不过,他们处理问题的方式与子问题之间的关系有关;(分治法中每个子问题是独立的)
贪心法
贪心法在求解问题时总能够做出当前最佳选择;不从整体上最有考虑,而仅仅是在某种意义上的局部最后解;一个采用贪心法的例子是霍夫曼编码,这是一个数据压缩算法;
近似法
近似法并不计算最优解,只计算出“足够好”的解;解决那些计算成本很高又因为本身价值很高而不愿放弃的问题;推销员问题是一个通常会用到近似法去解决的问题;(最优二叉树)
小酌软件工程:
模块化
在软件开发中黑盒代表一个模块,它的内部实现并不希望被使用这个模块的用户看到;用户只能通过模块设计者预定义好的公共接口和这个模块交互;
可读性
通过一些方法是程序变得可读;编写有意义的注释、实用贴切的标识符,编写自注释的代码;
简洁性
对问题本质而言简洁而清晰的解决方案;
一致性
建立编码约定并一直遵守这个约定;约定必须容易识别;
第二章 指针操作
在C语言中,对于任何类型T,我们都可以在T所在的内存地址处产生一个包含此对象地址的对应变量;
这实际是一种指向对象的变量,因此这些变量也称为指针;
指针是构建数据结构和操作内存的精确而高效的工具;当然它也很容易被勿用;
本章主要内容:
指针基础:
理解指针最佳方法-画图表;使用基本指针时如何避免空指针;
存储空间分配:
存储空间分配是指在内存中预留存储空间的过程;指针类似菜名,所指向内存空间中的数据对应实际的菜;
数据集合与指针的算数运算:
C中的数据集合主要指结构和数组;指针的算术运算定义指针的计算规则;
作为函数参数的指针:
通过这种方式,可以按照传递引用的方法传递函数参数;
指向指针的指针:
指向指针的指针,而不是执行具体变量的指针;指向指针的指针作为函数的参数来传递是非常普遍的;
泛型指针与类型转换:
是用来跨越和覆盖C语言的类型系统的途径;泛型指针指向某一数据而不需要理会数据的具体类型;类型转换允许临时地改变数据的类型;
函数指针:
指针指向可执行代码段或指向调用可执行代码段的信息快,而不是执行某种具体数据;他们把函数当做一下段数据来存储和管理;
下面让我们逐个介绍...
指针基础:
一个指针其实只是一个变量,他存储数据在内存中的地址而不是存储数据本身;
绘制图表示理解指针的最好方法之一:
指针通常都是按位置用箭头一个一个连接起来,当指针不指向任何数据(置为NULL)时,用两条竖线表示;
(P2_1)
对于其他类型的任何变量,除非显示指定,否则都不应该假设他指向的是一个有效地址;
C中,指针可以指向无效地址,这种指针被称为 悬空指针;
可能产生悬空指针的编程错误示例包括:
将任意的类型变量强制转换为指针变量;操作超出数组边界的指针;释放一个或多个仍被使用的指针等;
存储空间分配:
当在C中声明一个指针时,与声明其他类型的变量类似,一定量的存储空间会分配给这个指针;(指针的大小通常与编译器的设定以及某些特定的C实现中的类型界定符有关)
当声明一个指针时,仅仅只是为指针本身分配了空间,并没有为指针所引用的数据分配空间;
为数据分配存储空间的两种方法:
1)直接声明一个变量;
当声明一个一个变量的时候,编译器会根据变量的类型预留足够的内容空间;变量的存储空间是系统自动分配的,但此空间不会在程序的整个生命周期中永久存在,这一点处理自动变量尤为重要;(自动变量是一种在进入或离开模块或函数是其存储空间能够自动分配和释放的变量)
下面这个示例:
int f(int ** iptr){
int a = 10;
*iptr = &a;
return 0;
}
返回时,iptr会变成一个悬空指针:因为当函数返回时,变量a已经从栈中弹出,变成了一个不合法的变量;
2)另一种是在运行时动态地分配存储空间(例如,使用malloc或realloc);
动态分配存储空间时,会得到一个指向一个堆存储空间的指针;此存储空间有我们自行管理,并且会一直存在,除非我们显示地将它释放;
下面示例中,用malloc分配的存储空间会一直有效直到调用函数free来释放它:
#include <stdlib.h>
//参数iptr是一个指向我们想要改变其内容的对象的指针,此对象也是一个指针
int g(int ** iptr){
if((*iptr = (int *)malloc(sizeof(int))) == NULL)
return -1;
return 0;
}
所以,当函数返回时,此存储空间仍然有效,这一点与之前自动分配存储空间的变量完全不同;g返回时,iptr指向由malloc申请的地址空间;
(P2_2)
有动态内存分配所造成的内存泄漏问题:
动态分配了内存,但从未释放他,特别是重复执行代码时,问题尤为严重;(好在可以采用统一的内存管理方法来大大减少此类问题)
举例来说:
由用户来管理存储空间以及与存储空间相关的实际的数据结构,而数据结构自身只用于维护数据内部变量的存储空间的分配;
这样,在数据结构中,只使用指针所指向的数据变量,而不是此数据的私有副本;
这样做的意义在于:一个数据结构的实现并不依赖于他所存储的数据的类型和大小;同时,多个数据结构能够以单个数据形态表现;
初始化和销毁数据结构:
初始化可能涉及很多步骤,其中之一便是内存分配;销毁数据结构通常包括删除它所有数据,并释放数据结构所用到的内存;
之所以每个数据结构在初始化的时候都需要使用由用户提供的初始化函数,是因为数据存储的管理实际上是一种与具体应用相关的操作;
数据集合与指针的算数运算:
指针在C中最常见的用途就是用来引用数据集合;C支持两种数据集合:结构和数组(联合与结构类似,但一般单独归类);
结构:
结构通常使用各种各样的有序元素组成,从而看做单个连续的数据类型;结构指针是构建一个数据结构的重要组成部分;
如链表:
typedef struct ListElmt_ {
void * data;
struct ListElmt_ * next;
}
结构指针的使用:结构不允许包含自身的实例,但可以包含指向自身实例的指针;
数组:
数组是内存中连续排列的同类元素的序列;数组和指针密不可分:C会把数组转换为一个指向数组第一个元素的固定指针;
要访问数组第i个元素:
a[i] <=> *[a + i]
当对指针进行加一个整数i操作时,实际得到了一个地址,这个地址由a所在的地址加上数据类型a所含字节数乘以i得到;
(P2_3)
这也解释了为什么数组索引从0开始,因为数组的第一个元素在位置0;
C中多维数组是以主序的方式存储:右下标变化比左下标更快;
要访问二维数组第i行第j列,可以:
a[i][j] <=> *(*(a + i) + j)
作为函数参数的指针:
指针支持将参数作为引用传递给函数(即按引用调用);
按引用传递,当函数改变此参数时,这个被改变的参数的值会一直存在;
按值传递,此时值的改变只能持续到函数返回;
无论是否要改变函数的输入参数,使用指针传递大容量复杂的函数参数也是十分高效的手段;
按引用调用传递参数:
形式上,C只支持按值来传递参数;在按值调用传递参数的过程中,函数参数的一份私有副本将会用到函数的执行体中;使用指针参数传递给函数,可以模仿按引用调用传递参数;
示例:
void swap(int * x,int * y){
int tmp;
tmp = *x;
*x = *y;
*y = tmp;
return;
}
(P2_4)
使用数组作为参数时,数组的边界信息并不重要,编译器不要求数组有边界信息,但是提供边界信息对表达出函数内部处理该参数具有一定局限性是一种很有用的方法;
多维数组传递给函数时,除第一维以外,其他纬度的长度必须制定;
int g(int a[][2]){
a[2][0] = 5;
return 0;
}
原因在于,如果不指定第二纬的长度,数组将无法分行,因为不知道行长度;
作为参数指向指针的指针:
把指针当做参数传递给函数,是因为函数想要改变传递给它的指针;向函数传递一个待改变的指向指针的指针,函数返回后,该指针就指向了需要保留的值的地址;
示例:
int list_rem_next(List * list, ListElmt * element, void ** data);
(P2_5)
这是函数改变指向指针的指针的过程;
(void**)&iptr: iptr是一个指针变量,&iptr则是一个指针变量的地址,类型转换后,表示指向指针的指针变量;
泛型指针和类型转换:
待续。。。