type
status
date
slug
summary
tags
category
icon
password
C++编程
函数和参数
传值参数(Value Parameters)
- 形式参数: a, b, c
- 实际参数: 3, x, y
- 复制构造函数(copy constructor): 实参赋值给形参
- 析构函数(destructor): 释放形式参数,不影响实参
“形式参数”会增加程序的运行开销
在函数被调用时,复制构造函数把相应的实参分别复制到形参a,b和c之中,以供函数使用。而在函数返回时,析构函数会被唤醒,以便释放形参a,b和c
模板函数(Template Functions)
通常有两种形式:函数模板和类模板
- 函数模板: 针对仅参数类型不同的函数
- 类模板: 针对仅数据成员和成员函数类型不同的类
引用参数(Reference Parameters)
执行abc(x,y,z),这些实参将被分别赋予名称a,b和c
与传值参数的情况不同: 因为本体一样,只是名字不同,所以不仅没有复制实际参数的值,在函数返回时也没有调用析构函数
常量引用参数(Const Reference Parameters)
这种模式指出函数不得修改引用参数
这将立即告诉用户该函数并不会修改实际参数
🥸 编程建议: 对于诸如 int、float 和 char 的简单数据类型,当函数不会修改实际参数值的时候我们可以采用传值参数 对于所有其他的数据类型(包括模板类型),当函数不会修改实际参数值的时候可以采用常量引用参数
返回值(Return Values)
函数可以返回: 值,引用或常量引用
- 值: 均被复制到环境中
- 引用: 为返回类型添加一个前缀&
- 常量引用: 一个不变化的对象
递归函数(Recursive Functions)
递归函数是一个自己调用自己的函数
包括两种:
- 直接递归(direct recursion): 是指函数F的代码中直接包含了调用F的语句()
- 间接递归(indirect recursion): 是指函数F调用了函数G,G又调用了H,如此进行下去,直到F又被调用()
数学函数的递归定义:
阶乘函数 的递归形式:
完整的递归定义,必须包含:
- 基本部分: 一般包含了的情况。此时是直接定义的(即非递归)
- 递归部分: 的参数必须有一个比n小
归纳证明
步骤:
- 归纳初值
- 归纳假设
- 归纳步证明
例子: 证明
- 当 时,,显然成立
- 假设对于 时成立,即:
- 我们需要证明:
证毕
写成代码就是:
递归改为非递归
递归优点:
- 递归简洁、易编写、易懂
- 易证明正确性
但是递归效率低,重复计算多
- 改为非递归的目的是提高效率
- 单向递归可直接用迭代实现非递归
- 其他情形必须借助栈实现非递归(前期没学)
单项递归修改例子:
排列(Permutations)
定义:
- 表示n个元素的集合
- 为中移去以后所获得的集合
- 表示集合X中元素的排列方式
- 表示在中的每个排列方式的前面均加上以后所得到的排列方式
递归思路:
- 基本部分: n=1时,
- 递归部分: n>1时,
可以想想的情形
代码如下:
C++内存分配
4个内存区间如下:
- 栈: 在执行时需要的时候分配,在不需要的时候自动清除的变量存储区。里面的变量通常是局部变量、函数参数等
- 堆: 由new分配的内存块,他们要手动释放,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收
- 全局/静态存储区: 全局变量和静态(static)变量被分配到同一块内存中,他们共同占用这块内存区
- 常量存储区: 这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你也能通过非正当手段修改)
程序性能
空间复杂性
程序需要以下空间:
- 指令空间(Instruction space)
- 数据空间(Data space)
- 环境栈空间(Environment stack space)
空间复杂性度量
可以把一个程序所需要的空间分成两部分:
固定部分
- 指令空间(即代码空间)
- 简单变量及定长复合变量
- 常量
可变部分
- 复合变量空间:这些变量的大小依赖于所解决的具体问题
- 动态分配空间:这种空间一般都依赖于实例的特征
- 递归栈空间:该空间也依赖于实例的特征
看不懂这两个部分思密达 😖
所以任意程序P所需要的空间S(P)可以表示为
: 常量,代表固定部分
: 可变部分,代表可变部分
Examples
看几个例子更好懂:
顺序搜索(Sequenial Search)
- 采用实例特征n来估算该函数的空间复杂性
- 假定T为int类型,则
数组a中的每个元素需要2个字节,
实参x,n需要2个字节,
局部变量i需要2个字节,
每个整型常量0和-1也分别需要2个字节
- 所以
- 因为该空间独立于n,所以
下个例子:
每一次调用Rsum需要6个字节的栈空间(形参a和n,返回地址都要2个字节)。
由于递归的深度为n+1(最后一次n=0也算),所以需要6(n+1)字节的递归栈空间,因而
再来一个:
(注意返回值也要2个字节)
最后一个:
(int i也占2个字节)
真·最后一个:
时间复杂性
- 一个程序P所占用的时间
- 编译时间与实例的特征无关。因此主要关注程序的运行时间。运行时间通常用“”来表示
估算运行时间方法
- 关键操作计数(对时间复杂性的影响最大,确定这些关键操作所需要的执行时间)
- 确定程序总的执行步数
示例:
1. 求最大元素
2. 多项式求值(Polynomial evaluation)
3. 名次排序(Rank sort)
4. 选择排序(Selection sort)
5. 冒泡排序(Bubble sort)
6. 插入排序(Insertion sort)
7. 顺序搜索(Sequential search)
8. 名次排序优化/选择排序优化/冒泡排序优化
寻找最大元素
每次调用需要执行n-1次比较(i<n不算,只看循环里的)
多项式求值算法
加法的次数为n,乘法的次数为2n
“Horner法则“采用如下分解式计算多项式:
n次加法和n次乘法。更快!
计算排名
总的比较次数为:
名次排序
比较操作次数 =
排序移动操作次数 =
名次排序优化(不移回)
没有移回,移动操作次数 = n
选择排序
思想: 首先找出最大的元素,把它移动到a[n-1],然后在余下的n-1个元素中寻找最大的元素并把它移动到a[n-2],如此进行下去
每次调用需要执行size-1次比较,所以总的比较次数为。元素的移动次数为, 即:
比较操作次数 =
移动操作次数 =
冒泡排序
原理:
- 采用一种“冒泡策略”把最大元素移到右部。在冒泡过程中,对相邻的元素进行比较,如果左边的元素大于右边的元素,则交换这两个元素
- 在一次冒泡过程结束后,可以确信最大的元素肯定在最右边的位置上
顺序搜索
假定每个元素被查找的概率是相同的,比较次数 =
名次排序优化(原地重排)
名次排序存在的缺点? 需要额外创建空间u[]
直接根据排名r[i],将a[i]交换到规定的位置
- 从a[0] 开始,每次检查一个元素(r[i]为Rank)
- 如果当前正在检查的元素为a[i] 且r[i]= i,那么可以跳过该元素,继续检查下一个元素
- 如果r[i]≠i, 可以把a[i]与a[r[i]] 进行交换,交换的结果是把原a[i] 中的元素放到正确的排序位置r[i]上去。这种交换操作在位置i 处重复下去,直到应该排在位置i 处的元素被交换到位置i处
- 之后,继续检查下一个位置
最好情况:数组a最初已经有序。
最坏情况:数组a 形如 23451 或 51234
ㅤ | 最好 | 最坏 |
比较 | n(n-1)/2 + n | n(n-1)/2 + n |
移动 | 0 | 3*2(n-1) |
Rank 比较次数+ Rearrange比较次数=
选择排序优化
- 缺点:即使元素已经按序排列,程序仍然继续运行
- 为了终止不必要的循环,在查找最大元素期间,可以顺便检查数组是否已按序排列
ㅤ | 最好 | 最坏 |
比较 | n-1 | n(n-1)/2 |
移动 | 3 | 3(n-1) |
冒泡排序优化
如果在一次冒泡过程中没有发生元素互换,则说明数组已经按序排列,没有必要再继续进行冒泡过程
最好情况:数组a最初已经有序
最坏情况:数组a倒序
ㅤ | 最好 | 最坏 |
比较 | n-1 | n(n-1)/2 |
移动 | 0 | 3*n(n-1)/2 |
插入排序
思想:因为只有一个元素的数组是一个有序数组,所以可以从包含n个元素的数组的第一个元素开始。 通过把第二个元素插入到这个单元数组中,可以得到一个大小为2的有序数组。 插入第三个元素可以得到一个大小为3的有序数组
ㅤ | 最好 | 最坏 |
比较 | n-1 | n(n-1)/2 |
移动 | n-1 | n-1+n(n-1)/2 |
排序算法比较
算法 | ㅤ | 最好 | 最坏 |
名次排序 | 比较 | n(n-1)/2+n | n(n-1)/2+n |
ㅤ | 移动 | 0 | 6(n-1) |
选择排序 | 比较 | n-1 | n(n-1)/2 |
ㅤ | 移动 | 3 | 3(n-1) |
冒泡排序 | 比较 | n-1 | n(n-1)/2 |
ㅤ | 移动 | 0 | 3*n(n-1)/2 |
插入排序 | 比较 | n-1 | n(n-1)/2 |
ㅤ | 移动 | n-1 | n-1+n(n-1)/2 |
执行步数(Step Counts)
介绍:
- 利用操作计数方法来估算程序的时间复杂性忽略了所选择操作之外其他操作的开销
- 在统计执行步数的方法中,要统计程序/函数中所有部分的时间开销
- 与操作计数一样,执行步数也是实例特征的函数
程序步
介绍:
- 程序步可以定义为一个语法或语义意义上的程序片段,该片段的执行时间独立于实例特征
- 可以通过创建一个全局变量count 来确定一个程序或函数为完成其预定任务所需要的执行步数
- 把count引入到程序语句之中,每当原始程序或函数中的一条语句被执行时,就为count累加上该语句所需要的执行步数。当程序或函数运行结束时所得到的count的值即为所需要的执行步数
举例:
count = 2n+2
执行步数表
介绍
- 如果不想使用count计值语句,可以建立一张表,在该表中列出每条语句所需要的执行步数
- 建立这张表时,可以首先确定每条语句每一次执行所需要的步数以及该语句总的执行次数(即频率),然后利用这两个量就可以得到每条语句总的执行步数,把所有语句的执行步数加在一起即可得到整个程序的执行步数
语句每次执行所需要的步数通常记为s/e,一条语句的s/e就等于执行该语句所产生的count值的变化量
渐进符号
- Big Oh:
- Omega:
- Theta: (大写)
- Little Oh:
常见的g函数
g函数 | 名称 |
1 | 常数 |
logn | 对数 |
n | 线性 |
nlogn | n个logn |
n^2 | 平方 |
n^3 | 立方 |
2^n | 指数 |
n! | 阶乘 |
Big Oh
定义: 当且仅当存在正的常数和,使得对于所有的,有
- 加法规则
- 乘法规则
加法规则举例
时间复杂度为
注意: 为了使语句有实际意义,其中的应尽量地小。因此常见的是,而不是,尽管后者也是正确的
Omega
定义: 当且仅当存在正的常数和,使得对于所有的,有
举例
注意:仅是的一个下限。为了使语句更有实际意义,其中的应足够的大。因此常用的是及,而不是及,尽管后者也是正确的
Theta
定义: 当且仅当存在正常数和某个,使得对于所有的,有
举例
Little Oh
定义 当且仅当 且
举例
因为 且 ,所以
常用公式
⊕:
f(n) | 渐进符号 |
(k>0) | |
(r>1) | |
和与积的引用规则
Examples
递推求和
递归求和
矩阵加法
矩阵转置
顺序搜索
排列
- 假定
- 当时,所需要的时间为
- 当时,将执行else语句。 此时,for循环将被执行次。 由于每次循环所花费的时间为, 因此,当时,。 直接代入,得到
折半搜索
- 每次while循环(最后一次除外)都将以减半的比例缩小搜索的范围,所以该循环在最坏情况下需执行次
- 由于每次循环需耗时,因此在最坏情况下,总的时间复杂性为
有关数据结构的问题,欢迎您在底部评论区留言,一起交流~
- Author:zmy10032
- URL:zmy10032.top/article/data-structure
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!