1.算法原地工作是指辅助空间不随着数据规模的增大而增大,不是说不需要辅助空间
2.栈和队列属于逻辑结构而非存储结构,它们的实现才属于存储结构
3.数据元素是数据的基本单位,数据项是数据的最小单位
4.程序需要算法和数据结构结合在一起才能实现,仅仅把算法用某种计算机语言来描述不能称之为程序
1.链表头节点的作用:如果没有头节点,那么在头部插入一个节点和在中间插入一个节点的操作是不一样的,如果加上头节点就可以去除这样的判断,代码更加简化了。
2.线性表最开始的节点没有前驱,最后一个节点没有后继
3.循环链表的优点:从任一节点出发都可以访问到链表中的每一个元素
4.循环链表不同于循环队列,循环队列如果头指针和尾指针在最后重叠到了一起,无法判断队列已满,所以决定牺牲一个位置,让尾指针实际上为尾后指针
1.涉及到表达式的题目,关键就在于考虑运算符的优先级,当前符号会把符号栈中优先级更高的元素全部执行完,然后自己进栈,这是不难理解的
2.队列删除元素是在队首,添加元素是在队尾
3.输出受限的双向队列意思是只能一端输出,两端都可输入,输入受限是一个意思
4.尾递归和单向递归可以使用循环来消除递归。尾递归就是递归语句在最后,当前环境的情况不需要保留。即使保留了也没什么用,所以不需要单独开辟堆栈来进行下一次递归,下一次的递归直接在本次递归的空间进行即可。注意这里说的是能够用循环消除递归的条件。不是说消除递归的条件,事实上任何递归都可以转化为非递归
5.n个数的合法出栈序列种类数是卡特兰数
6.一般在队列中,头指针指向的都是有效元素,尾指针是最后一个有效元素的后继指针
7.在共享空间的栈中,指针指向的好像都是有效数据,但在一个栈中尾指针应该是尾后指针
8.假溢出:头指针没有指向最开始的位置,尾指针指向最后位置,呈现出队满的假象,所以引入循环队列
9.循环队列应该是用顺序存储机构,所以头尾指针的变化都是通过数的运算得来的,没有什么next指针
我觉得首地址的意思是前一个元素的地址,地址是当前元素的地址,
在已知某元素的地址BA时,求从开始起第i个元素的地址的公式为BA + (i - 1)* 数据元素大小
题型1:已知首地址为BA,求另一元素元素的首地址:BA + (当前元素前元素的个数) * 数据元素大小
题型2:
三对角矩阵:只有主对角线,低对角线,高对角线非0,其余元素均为0
因为三元组是为了压缩稀疏矩阵,所以原矩阵的所有信息都需要保留,所以它除了存储数据还需要存储原矩阵的行数,列数,总元素个数,所以计算存储空间时要注意
广义表在head()和 tail()时,如果是head就是直接取出第一个元素(原子或者子表)即可,如果是tail(),则实际上是用()把除了第一个元素外的其它元素括起来。因为广义表的定义就是最后的元素一定是子表,不管是原子还是子表,都要统一成一个子表(官方讲就是广义表除非是空表,否则一定存在表头和表尾,而且表尾一定是一个广义表
广义表的长度:数据元素的个数(原子和子表都算一个元素)
广义表的深度:从左向右左括号的数量 或 从右向左右括号的数量(就是嵌套的层数)
解析中的第三点不太懂
一个稀疏矩阵Am*n 采用三元组形式表示,若把三元组中有关行下标与列下标的值互换,并把m和n的值互换,则就完成了Am*n 的转置运算(B)。
A.对
B.错
解析:三元组转置: (1)将数组的行列值相互交换 (2)将每个三元组的i和j相互交换 (3)重排三元组的之间的次序便可实现矩阵的转置。
三维数组如何计算个数:如果是行优先,那么策略是首先是第一行,然后是第一列,此时如果坐标都是从0开始,指向的就是二维平面中【0,0】这个点,此时会把z轴上的所有元素都进行填充,之后到【0,1】,再把这个点对应的z轴上的元素进行填充,以此类推,进行完第一行进行第二行。。。如果是列优先,那么也是先确定列和行从而确定一点然后填充z轴
中序遍历:中序遍历就像在无风的情况下,太阳直射,将所有的结点投影到地上。顺序为左子树、根、右子树。如图 所示。图中的二叉树,其先序序列投影如图所示。中序遍历序列为:DBEAFGC
先序遍历:先序遍历就像在左边大风的情况下,将二叉树树枝刮向右方,且顺序为根、左子树、右子树,太阳直射,将所有的结点投影到地上。图中的二叉树,其先序序列投影如图所示。先序遍历序列为:ABDECFG
后序遍历:后序遍历就像在右边大风的情况下,将二叉树树枝刮向左方,且顺序为左子树、右子树、根,太阳直射,将所有的结点投影到地上。图中的二叉树,其后序序列投影如图所示。后序遍历序列为:DEBGFCA
中序转后序:
策略:从前向后遍历中序表达式
1.字母:直接输出到答案中
2.(:进栈
3.符号;弹栈,直到找到优先级比它低的符号或者到(不再弹栈(弹出的是>=此符号优先级的)
4.);弹栈,直到(,将(弹栈
#include
#include
#include
#include
原理:优先级高的元素应该先输出,栈顶始终维护当前优先级最高的符号,能够保证最先输出
中序转前序
策略:从后向前遍历中序表达式
1.字母:直接输出到答案中
2.):进栈
3.符号;弹栈,直到找到优先级比它低或相等的符号或者到)不再弹栈(弹出的是优先级>此符号的)
4.( : 弹栈,直到),将)弹栈
最后一步翻转现有答案序列
注:一定要注意转后序和前序时,符号弹栈优先级的区别。
后序表达式
和前序表达式的计算方法类似,但是是从左到右扫描表达式,将离运算符最近的两个数据进行运算,然后填充到原位置,直到扫描完成。
前序表达式
对于一个前序表达式的求值而言,首先要从右至左扫描表达式,从右边第一个字符开始判断,如果当前字符是数字则一直到数字串的末尾再记录下来,如果是运算符,则将右边离得最近的两个“数字串”作相应的运算,以此作为一个新的“数字串”并记录下来。一直扫描到表达式的最左端时,最后运算的值也就是表达式的值。例如,前序表达式“- 1 + 2 3“的求值,扫描到3时,记录下这个数字串,扫描到2时,记录下这个数字串,当扫描到+时,将+右移做相邻两数字串的运算符,记为2+3,结果为5,记录下这个新数字串,并继续向左扫描,扫描到1时,记录下这个数字串,扫描到-时,将-右移做相邻两数字串的运算符,记为1-5,结果为-4,所以表达式的值为-4。
孩子表示法
3.孩子兄弟表示法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x6052Wnf-1615264890084)(https://sc02.alicdn.com/kf/H46a033a5bf8e4e2f9ff279ff7536bbb7T.jpg)]
树转化为二叉树:留长子,养兄弟
特点:根节点一定没有右子树,因为只留下了一个长子,而且经过结构调整之后即使在右面的孩子也跑到了左边,原来互为兄弟的节点变化之后就变成了父子节点
森林转化为二叉树
二叉树转化为树
遍历所有节点,如果此节点有左孩子,则将此左孩子的右孩子,此左孩子的右孩子的右孩子。。。都和此节点连线
去除所有节点和其右孩子的连线
调整结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vwngr070-1615264890085)(https://ae02.alicdn.com/kf/Hbb4e7e0909f14275b567ffc231e346dcH.jpg)]
二叉树转化为森林
这里需要判断一下此时的二叉树到底是转化为树还是转化为森林,如果此时的二叉树是由树转化而来的,那么根节点一定是没有右子树的,所以只需判断根节点是否有右子树即可。如果有,则可转化为森林,如果没有,则转化为树
证明:
深度为k的完全二叉树节点数最少的情况是深度为 k − 1 k-1 k−1 的满二叉树加上第k层的1个 节点,此时节点个数为 2 k − 1 2 ^ {k - 1} 2k−1
最多的情况为深度为k的满二叉树,此时节点个数为 2 k − 1 2 ^ k - 1 2k−1
所以可得,深度为k的完全二叉树的节点个数n满足, 2 k − 1 < = n < = 2 k − 1 2 ^ {k - 1} <= n <= 2 ^ k - 1 2k−1<=n<=2k−1
可得: l o g 2 ( n + 1 ) < = k < = l o g 2 n + 1 log_2(n + 1) <= k <= log_2n + 1 log2(n+1)<=k<=log2n+1
所以k取 ⌊ l o g 2 n ⌋ + 1 \lfloor log_2n \rfloor + 1 ⌊log2n⌋+1
一般所说的哈夫曼树只有度为0和2的节点
度为m的哈夫曼树:只有度数为有0和m的节点
哈夫曼树不一定是完全二叉树
树有先根遍历和后根遍历两种方法,先根遍历:先访问树的根节点,再依次先根遍历子树;后根遍历:先依次后根遍历子树,再访问树的根节点。之所以没有中序遍历是因为树未必就是二叉树,子树有多个,可选择的序列也就有多个(比如根节点下有3个子树,分别为a,b,c,不考虑根节点的遍历顺序,仅这3个子树就是 3 ! 3! 3! 种遍历顺序),所以仅考虑是先遍历子树还是先遍历根节点这两种方式。而树的先根遍历与对应二叉树的中序遍历结果是一样的,理由我还没想懂。
若二叉树采用二叉链表存储结构,要交换其所有分支结点左、右子树的位置,利用©遍历方法最合适。A.前序 B.中序 C.后序 D.按层次
这道题现在不会
具有N个结点的二叉树,采用二叉链表存储,共有___n+1___个空链域
n个结点的二叉链表中,有2n个链域,每一条非空链域对应一条树枝,而树支的个数为n-1,因此,空节点个数为2n-(n-1)=n+1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-okIWyY4q-1615264890086)(https://ae04.alicdn.com/kf/H50117ce8a53b427fbf121b7b8d1a7e754.jpg)]
二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成,是有序树。
二叉树的左右子树必须是不相交的,所以
每个结点至多有两棵子树的树就是二叉树
上面这种说法是错误的,因为没有指出左右子树是不相交的。
其次,二叉树是有序树,即二叉树的分支具有左右次序,不能随意颠倒,但是这个“有序”我还没太明白是怎么个有序法。
不能说二叉树是树的特殊形式,因为我们在说A是B的特例时,必须保证B能够满足A的所有性质,A只是确定了某些条件之后的B,但是树的性质并不能包含二叉树的一部分性质,比如二叉树节点的最大度数为2,树则没有这个限制,二叉树的节点存在左右之分,但树则不存在,两者在定义上都存在差异。
完全图:
连通分量:无向图的极大连通子图。任何连通图的连通分量只有一个,即是其自身
强连通分量:有向图的极大强连通子图
路径长度:路径上边或弧的数量
邻接矩阵:
邻接表:
逆邻接表:
十字链表:有向图的存储结构。
邻接多重表:无向图的存储结构。邻接表 + 十字链表,更加复杂了
引入的原因:使用邻接表存储无向图时,对图中某节点进行操作进行修改,修改操作两个节点效率较低
关键路径不唯一时,一条关键路径上的活动提前完成只能使得这条关键路径变为非关键路径,其它关键路径没受到影响,所以整个工程未必就会提前完成。
强连通分量是有向图的概念;连通分量是无向图的概念
邻接多重表是无向图的概念,它的引入就是为了解决邻接表存储无向图时的一条边的重复存储的问题
十字链表是有向图的存储结构
连通图才存在生成树,非连通图可以生成森林
不同的求最小生成树的方法最后得到的生成树是相同的,这句话是错的,原因是如果存在相同权值的边,结果未必唯一。如果能保证权值均不相同则结果唯一
求最小生成树的普里姆(Prim)算法中边上的权可正可负。这道题的结果存在异议。
AOV(A:Activity活动 O:on位于 V:Vertex顶点)AOV网中,结点表示活动,边表示活动间的优先关系,边无权值,即拓扑排序网络
AOV网特点:有且只有一个表示开始状态的初始点和一个表示结束的终点,始点入度为0,终点出度为0
AOE(E:edge边) 中,结点表示活动开始或结束的状态,边上的权表示活动进行所需时间,关键路径就是AOE网中的概念
顺序查找
查找成功:$ \frac {n + 1}{2}$
每个元素被查找的概率都是$ \frac {1} {n} , 从 第 一 个 元 素 都 第 n 个 元 素 需 要 的 查 找 次 数 依 次 为 1 , 2 , 3 , . . . n , 所 以 加 在 一 起 就 是 ,从第一个元素都第n个元素需要的查找次数依次为1,2,3,... n,所以加在一起就是 ,从第一个元素都第n个元素需要的查找次数依次为1,2,3,...n,所以加在一起就是 \frac {n (n + 1)} {2} , 所 以 平 均 查 找 长 度 就 是 ,所以平均查找长度就是 ,所以平均查找长度就是 \frac {n + 1}{2}$
查找失败: n n n
只扫一遍序列,比较n次即结束了查找,所以是n
折半查找
此类问题会给定数据的个数(n),折半搜索的过程类似一个二叉树向下分叉的过程,我们知道n个的节点的完全二叉树的深度为$ \lfloor log_2^n \rfloor + 1$,所以我们首先可以确定这颗树的高度,然后确定最后一层有几个数。
查找成功:如果查找的是第一层的元素,只有一个节点,那么比较次数就是1,如果查找的是第二层的元素,一共两个元素,每个元素的比较次数是2次,总共就是2 * 2 = 4次,以此类推可以算出总共的比较次数,在每个元素的查找概率相同的情况下,总比较次数\数据个数即为答案
公式计算过程:
折半搜索总节点数与对应判定树高度关系 n = 2 h − 1 n = 2 ^ h - 1 n=2h−1
由此可得 h = l o g 2 ( n + 1 ) h = log_2(n + 1) h=log2(n+1)
根据查找成功时的计算方法可得 ASL = $ \frac{1}{n} \sum_{i = 1}{h}(2{i - 1} * i)$
这里需要把求和公式展开得到Sn,然后左右两边同时乘以2得到2Sn,上下相减即可求得Sn
计算的时候注意区分h和n,最终算出来就是上面图片中的结果
查找失败:我们需要把整个二叉树画出来,就如同下面这样。查找失败也就是查找的是那些空格的平均查找长度,计算方法和上面是一样的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4YAVTChB-1615264890092)(https://ae02.alicdn.com/kf/Hfe88f5aadae34147b87451b2822a386dH.jpg)]
哈希查找
需要根据实际情况进行计算,和折半查找一样根据题目去做即可。
但是做法是固定的,
查找成功平均查找长度的计算,首先需要计算出类似下面这样的一张表格,其中的探测次数就是包含第一次索引和后面解决冲突时的比较次数加在一起的总次数。平均查找长度即把所有的探测次数求和除以数据总个数即可。
查找失败需要注意的一点就是一次查找应何时结束,当我们找到的数指和待找值不相等不能直接判定元素不存在,因为待找值有可能为了解决哈希冲突所以后移了,还需要往后去找。如果找到空的位置了,查找就可以停止了,说明这个待找值是不存在的,因为如果是为了解决冲突,既然这个位置是空的,那么待找值完全可以填充到这里,既然没有填充说明它是不存在的。
所以计算的方法就是:假设我们的待找值通过哈希函数依次到0 ~ 12的地址处,如果这个位置为空,那么比较次数就是0,直接进行下一个位置,如果这个位置不为0,那么需要一直往后走,直到为空或遍历完所有位置,加上比较次数,往后一直这么进行下去。
分块查找
假设n个数据分为m个数据块,每个块内有t个数据,即 m ∗ t = n m * t = n m∗t=n
如果对索引采用顺序查找则 A S L = m + 1 2 + t + 1 2 ASL = \frac {m + 1}{2} + \frac {t + 1}{2} ASL=2m+1+2t+1,根据不等式的性质,当 m = t = n m = t = \sqrt{n} m=t=n 时,ASL取得最小值 n + 1 \sqrt{n} + 1 n+1
注:关注二分查找失败和哈希查找失败时ASL的计算过程可以发现一个区别,判定元素不存在的条件都是找到空位置,但是哈希查找的比较次数中是包含了判定是否为空的那一次,但是二分查找就没有包含。这是因为二分查找当左右指针重合时就停止了,空位置的空是通过左右指针相遇来体现的,所以并没有最后判空那一次比较,但是哈希查找的判空是真实存在的。
开放地址法 ( H a s h ( K e y ) + d i ) m o d m (Hash(Key) + d_i) mod m (Hash(Key)+di)modm
易错点:1. 注意mod的是m(表长),而不是求哈希值mod的那个东西了 2. 公式是基于hash(key)进行变化的,不是对原数进行变化
再哈希法:就是对出现的重复的哈希值再进行一次运算
链地址法:类似邻接表的形式,把所有映射值相同的元素都放到内部一个链表中
公共溢出区法:把出现冲突的数据直接放在另一个空间中存储起来
AVL树:自平衡二叉查找树,一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树
节点的平衡因子:节点的左子树的高度减去它的右子树的高度
装填因子: α = 填 入 表 中 记 录 个 数 哈 希 表 长 度 α = \frac{填入表中记录个数}{哈希表长度} α=哈希表长度填入表中记录个数,α越大,产生冲突的可能性越大,平均查找长度也就越大,这也就解释了“不懂的知识“中的第二个问题
二叉排序树:若左子树不为空,则左子树上节点的值均小于根节点,若右子树不为空,则右子树上节点的值均大于根节点,左右子树均为二叉排序树(这是一个递归的定义)
// 二分的本质在于边界而非单调性,存在单调性的可以用二分,但是没有单调性的未必就不能用二分
#include
using namespace std;
const int N = 1e5 + 10;
int a[N];
int main()
{
int n, x;
cin >> n >> x;
for (int i = 0; i < n; ++ i) cin >> a[i];
//-------------------------------------------寻找>=x中位置最靠前的那一个数据(最后结果是>=x的)
int l = 0, r = n - 1;
while (l < r)
{
int mid = l + r >> 1;
if (a[mid] >= x) r = mid;
else l = mid + 1;
}
if (a[l] == x) cout << l << endl;
else cout << "不存在" << endl;
//--------------------------------------------------------------------------------------
//-------------------------------------------寻找<=x中位置最靠后的那一个数据(最后结果是<=x的)
l = 0, r = n - 1;
while (l < r)
{
int mid = l + r + 1 >> 1;
if (a[mid] <= x) l = mid;
else r = mid - 1;
}
if (a[l] == x) cout << l << endl;
else cout << "不存在" << endl;
//--------------------------------------------------------------------------------------
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dezkpt6B-1615264890094)(https://sc04.alicdn.com/kf/H27d340a6eea548c4b1ddd9c8c2d63c0ad.jpg)]
插入排序: 直接插入;希尔排序
选择排序: 直接选择;堆排序;
交换排序: 冒泡排序;快速排序;
归并排序
基数排序
不同的地方找到的都不一样,但基本就这些了,不妨结合着来看。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fgh9Et8p-1615264890094)(https://ae02.alicdn.com/kf/H369c3ee8dd684adea3b451aac2d348d5z.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m6iyMs4O-1615264890095)(https://ae04.alicdn.com/kf/H3525a5d110d54914a51225091576adb1P.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MwvPVTSR-1615264890096)(https://ae04.alicdn.com/kf/H3d2dc206b3644fd997b03dac4ec5463fO.jpg)]
时间复杂度和初始状态无关:堆,归并,选择,基数(一堆乌龟选基友)
比较次数和初始状态无关:选择,基数
移动次数和初始状态无关:归并,基数
情况2、3是必定包含在情况1中间的
堆排序
思想:首先对初始数组建立最小堆,然后取堆顶元素与堆尾交换,再此堆元素(不含堆尾)再重新构建最小堆,依次循环。
分析:由于建立最小堆其实就是将初始元素按照规定的准则进行一系列排序(包括层级向下比较、交换),所以如果元素一开始就已经是最小堆则不需要此时的交换且大大减少向下比较次数,
所以堆排序不属于情况二也不属情况三
归并排序
思想:将初试数组划分成N个子数组,两两进行合并排序,然后结果再和其他同级合并后的数组合并知道合并完所有。
分析:外层递归与初始无关,主要思考合并排序中的比较和交换即可。合并排序思想:将数组A第一个与数组B第一个比较,较小的那一个直接进入result数组并且指针向下移动再与对面数组第一个比较,依次类推,然后将还有剩余的数组内元素全放入result,最后用result将原数组中对应的值一一替换。因此,假设初始数组就是有序的,那么每次合并排序的时的比较次数都仅仅是一个待合并的数组的长度,因此比较次数与初始状态有关,归并排序不属于情况二
然而,不论一开始的状态如何,最后都是两个数组进入result,移动次数都为两个待合并数组的长度和,然后再将result内元素全部移动到原来数组进行替换。所以元素移动次数与初始状态无关,归并排序属于情况三
选择排序
思想:i 从头开始,每次遍历之后所有的元素,k 从 i 开始,向后标记最小的元素,循环后如果大于 i ,则与 i 位置元素交换,一直到最后。
分析:比较次数都是N-1的阶乘,与初始状态无关,所以选择排序属于情况二
交换次数当全部已经排序好时则不发生交换,所以选择排序不属于情况三
基数排序
思想:将数组从低位到高位,每到一位对应分入10个桶(0-9)中,依次到最高位,由于每上升一位,处于“0号桶”中的数据都会将此位之前的数字排好,以此达到排序效果。
分析:基数排序中并不发生任何元素之间的比较,所以基数排序属于情况二
不论初始数组如何排列,都是从个位开始,各自进入自己个位对应的位置,之后也都是一样,所以元素移动次数一样,所以基数排序属于情况三
以下代码均为C++实现,但是并不会采用STL(Standard Template Library)中的容器,所以其它语言较易迁移实现
插入排序
直接插入排序
#include
#include
using namespace std;
void direct_insert(vector &a)
{
int size = a.size();
for (int i = 0; i < size; ++ i)
{
int pos = i;
for (int j = i - 1; j >= 0; -- j)
{
if (a[i] >= a[j]) break; // 等号保证了排序的稳定性
pos = j;
}
if (pos != i)
{
int tmp = a[i];
for (int j = i; j > pos; -- j) a[j] = a[j - 1];
a[pos] = tmp;
}
}
}
int main()
{
int n;
cin >> n;
vector a(n);
for (int i = 0; i < n; ++ i) cin >> a[i];
direct_insert(a);
for (int i = 0; i < n; ++ i) cout << a[i] << " ";
cout << endl;
return 0;
}
希尔排序
选择排序
简单选择排序
/**
* 算法本身不难理解,但难点在于如何优化
*
* 优化就是说在序列变为有序后如何及时判断出来并停止后续的无效遍历
* 不管怎么搞,我们一定会始终维护一个最小值,我第一次想的策略是从前向后如果在后面找到
* 一个比最小值小的元素说明序列一定是无序的,但是我忽略了一个问题,如果后续没有比当前最小值
* 更小的元素,那么后面的序列即使无序我们也是判断不出来的,出现这个问题原因是我们始终能够判断的是
* 当前值和最小值的相对关系,没有判断当前值和当前值到最小值中间这些数的相对关系
*
* 所以采用从后向前遍历,我们可以保证如果当前遍历到的位置如果不是当前最小值说明序列仍无序,否则更新当前最小值
* 这么说起来有些抽象,看看代码或许就懂了
*/
#include
#include
using namespace std;
const int N = 1e6 + 10;
int n;
int a[N];
void direct_choose(int a[], int l, int r)
{
int len = r - l + 1;
for (int i = 0; i < len; ++ i)
{
bool sorted = true;
int min_pos = len - 1;
for (int j = len - 1; j >= i; -- j)
{
if (a[j] > a[min_pos]) sorted = false;
else min_pos = j;
}
if (sorted) break;
if (i != min_pos) swap(a[i], a[min_pos]);
}
}
int main()
{
cin >> n;
for (int i = 0; i < n; ++ i) cin >> a[i];
direct_choose(a, 0, n - 1);
for (int i = 0; i < n; ++ i) cout << a[i] << " ";
return 0;
}
堆排序
/**
* 递归版堆排序
* 思路:
* 我们采用小根堆,也就是上面的数据小下面的数据大的结构
* 每次把剩余数中的最小值和最后一个数据交换一下,然后序列长度减1,维护小根堆
* 假设序列长度为n,进行n次上面的过程完成排序
* 结果是一个递减序列
*/
#include
#include
using namespace std;
const int N = 1e5 + 10;
int n;
int h[N];
void down(int h[], int len, int u)
{
int t = u;
if (u * 2 <= len && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= len && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (t != u)
{
swap(h[u], h[t]);
down(h, len, t);
}
}
int main()
{
cin >> n;
for (int i = 1; i <= n; ++ i) cin >> h[i];
for (int i = n >> 1; i >= 1; -- i) down(h, n, i); //O(N)的建堆操作:从最后一个非叶节点开始下沉操作
int t = n;
while (t)
{
swap(h[1], h[t --]);
down(h, t, 1);
}
for (int i = n; i >= 1; -- i) cout << h[i] << " ";
return 0;
}
交换排序
冒泡排序
#include
#include
using namespace std;
const int N = 1e6 + 10;
int n;
int a[N];
void bubble_sort(int a[], int l, int r)
{
int len = r - l + 1;
for (int i = 0; i < len - 1; ++ i)
{
bool flag = true; //实现优化
for (int j = 0; j < len - i - 1; ++ j)
{
if (a[j] > a[j + 1]) // 不含=保证排序的稳定性
{
flag = false;
swap(a[j], a[j + 1]);
}
}
if (flag) break;
}
}
int main()
{
cin >> n;
for (int i = 0; i < n; ++ i) cin >> a[i];
bubble_sort(a, 0, n - 1);
for (int i = 0; i < n; ++ i) cout << a[i] << " ";
return 0;
}
快速排序:下面的两种写法,前者是考试时问到第几轮排序后的结果是多少对应的程序,后者是算法题中惯用的写法,不知道为什么在acwing的平台上前者在某些数据点会超时,虽然我觉得这些写法在常数上相差不大,应该不会出现这种问题。
// 数据结构课程中的写法
#include
#include
using namespace std;
const int N = 1e6 + 10;
int n;
int a[N];
void quick_sort(int a[], int l, int r)
{
if (l >= r) return ;
// 横线中的就是一般书籍中另写的函数内容,这里为了简洁和减少函数调用所耗时间省略了
//------------------------------------------------------------------------
int pivotPos = l, pivot = a[pivotPos], i = l, j = r;
while (i < j)
{
while (a[j] >= pivot && i < j) -- j;
a[i] = a[j];
while (a[i] <= pivot && i < j) ++ i;
a[j] = a[i];
}
a[i] = pivot;
//------------------------------------------------------------------------
quick_sort(a, l, i - 1);
quick_sort(a, i + 1, r);
}
int main()
{
cin >> n;
for (int i = 0; i < n; ++ i) cin >> a[i];
quick_sort(a, 0, n - 1);
for (int i = 0; i < n; ++ i) cout << a[i] << " ";
return 0;
}
// 非数据结构课程中的写法
#include
#include
using namespace std;
const int N = 1e6 + 10;
int n;
int a[N];
void quick_sort(int a[], int l, int r)
{
if (l >= r) return ;
int x = a[l + r >> 1], i = l - 1, j = r + 1;
while (i < j)
{
while (a[++ i] < x);
while (a[-- j] > x);
if (i < j) swap(a[i], a[j]);
}
quick_sort(a, l, j);
quick_sort(a, j + 1, r);
}
int main()
{
cin >> n;
for (int i = 0; i < n; ++ i) cin >> a[i];
quick_sort(a, 0, n - 1);
for (int i = 0; i < n; ++ i) cout << a[i] << " ";
return 0;
}
归并排序
#include
using namespace std;
const int N = 1e6 + 10;
int n;
int a[N], tmp[N];
void merge_sort(int a[], int l, int r)
{
if (l >= r) return ;
int mid = l + r >> 1;
merge_sort(a, l, mid);
merge_sort(a, mid + 1, r);
int i = l, j = mid + 1, k = 0;
while (i <= mid && j <= r)
if (a[i] <= a[j]) tmp[k ++] = a[i ++];
else tmp[k ++] = a[j ++];
while (i <= mid) tmp[k ++] = a[i ++];
while (j <= r) tmp[k ++] = a[j ++];
for (int i = l, j = 0; i <= r; ++ i, ++ j) a[i] = tmp[j];
}
int main()
{
cin >> n;
for (int i = 0; i < n; ++ i) cin >> a[i];
merge_sort(a, 0, n - 1);
for (int i = 0; i < n; ++ i) cout << a[i] << " ";
return 0;
}
基数排序