算法概念

来源:互联网 发布:网易企业邮箱域名 编辑:程序博客网 时间:2024/05/17 02:50

递归:

递归常见的有树的遍历、图中广度优先和深度优先查找,以及排序

递归有普通递归和尾递归,普通递归分为递归阶段和回归阶段,中间通过终止条件限定,尾递归多了一个参数,而且是最后执行,它的返回值不属于表达式的一部分,回归过程中不用做任何操作

普通递归在C语言中的栈中执行,栈顶包括有输入参数、返回值、临时存储空间、保存的状态信息和输出参数,而栈是后进先出的特点,可以用来实现递归,问题是会占用大量时间和空间,因此尾递归就出现了


对比就是4的阶乘,不依赖返回参数,不会占用空间,最大限度减少了时间和空间使用



复杂度:

O表示一定条件约束下函数的上限值,它考虑的是趋于无穷时候的算法,所以常量可以忽略不计,只考虑高阶项因子,如果算法的复杂度比其他算法的复杂度都低,并且没有过多常数项,就算是高效的算法,但有时候复杂度大且常量小的算法会比复杂度小但常量大的算法性能更好,比如O(n)=400n,O(n)=n的平方,那么n<400,第二个算法效率会高一些

复杂度递增:

O(1)     一个数据集中获取第一个元素

O(lg n) 数据集分开两半,再将分开的分成两半,以此来推

O(n)     遍历一个数据集

O(n lg n)  数据集依次分开两半,同时遍历每一半数据

O(n的平方) 遍历一个数据集每个元素的同时遍历另一个数量级相同的数据集

O(2的n次方)  为数据集生成所有可能的子集

O(n!)  为数据集生成其可能的所有排列组合



链表

单链表:数据成员+next指针,它在内存中是分散排列的,只不过是顺序指向

双向链表:prev+数据+next

循环链表:分单双向,但尾部不是null,如果是双向的,头部的prev指向尾部,尾部的next指向头部

补充:数据结构中包含size和tail,这样访问尾部元素和统计链表节点个数的时候就变成了O(1)的操作

数组vs链表:链表适合增删改操作,是O(1)的,数组适合随机访问,因为它是连续内存存储,反过来,链表不适合精确访问,如果是单向链表就得遍历整个链表,双向也会,而增删改操作对于数组是O(n),因为连续操作任何一个元素变动都会导致其余所有元素变动;但是,如果给链表的一个特定元素后面插入一个元素,访问会使复杂度变成O(n),插入动作是O(1)操作

单向vs双向,单向链表不能依据指定元素删掉某个元素,因为每个元素没有prev指针,因此也就无法将前驱结点的next指针指向被移除元素的后继节点;而且,单向链表和双向链表的头尾部插入元素处理方式不同,单向链表要设置为null,双向链表不必,因为它有指向,双向链表可正反向遍历



栈、队列和集合

栈和队列顺序分别是后进先出和先进先出

栈通过链表来实现,必要时创建临时栈来倒腾数据,包含栈的大小和栈顶元素,

队列先进先出是通过出队和入队完成的,入队排队尾,出队删除头部元素,包含头元素信息和队列大小。双向队列能够在头部和尾部都进行插入和删除,循环队列没有队尾,队列最后一个元素指向队列第一个元素

对于栈和队列来说,不常进行遍历操作,而且采用链表的增删操作比它们的频繁出入操作要更简单


多态:某种类型的对象可以用其他特定的对象替代使用,这两种对象之间有某些相同的特性


集合是不同成员(唯一)的无序聚集,包含并交补、从属关系、集合个数和是否相等

实现集合的好方式就是链表,比如需要移除和新增数据的时候,就可以不用遍历数据直接进行操作,遍历数据是因为数据的唯一性进行标识,这样效率比较低,而链表效率高

集合覆盖算法,包括待覆盖的集合S、返回的集合C和待选集合中的子集P A1 A2 ..,每次找到P的子集中能覆盖S的最大交集,然后加到覆盖集C中并把它的成员从S中删除,如果S不为空,说明P中的子集不能覆盖S,而如果P中的子集没有与S的交集,也表示P中的子集不能覆盖S,其中C的复杂度是O(m的3次方),因为最大计算情况下,P中都只有唯一一个子集与S对应,而S有每个子集,而当求S和P的交集的时候,P的每个子集都只有唯一一个成员需要遍历,所以内层是O(m的平方)的,外层执行m次,也就是m的3次方


集合的操作,判断是否为子集也能通过交集和是否相等是来实现,判断是否相等也能通过差集和个数判断来实现,交集也能通过差集来实现,插入和删除复杂度是O(n)比起链表的O(1)要复杂的多;多重集合不用遍历整个集合检查唯一性,所以插入操作是O(1),但移除仍是O(n),因为要搜索整个集合找到要移除的那个成员,并集是m*n的复杂度,如果明确两个集合没有重复,就能通过链表的连接操作将结果插入到最终的集合,这样复杂度就是m+n次,


哈希表:

hash是一种检索方法,就是散列。哈希表支持随机访问,但对于顺序访问效率较低,解决冲突和分布均匀是哈希算法好坏的标准,哈希分为链式哈希和开地址哈希,


链式哈希是一个桶的概念,当每个桶冲突的时候就放入桶中更深的地方(拉链法),负载因子是 元素个数/桶个数,好的哈希分布应该是各个桶同时增长,删除的复杂度是O(m),m是桶的个数,其他是O(1);最不好的性能是所有元素都在一个桶中,也就是O(n),如果分布均匀,能在固定时间内找到元素,就是O(1).

开地址哈希没有桶的概念,每个槽位对应一个值,这对于固定大小的表应用来说非常有用,解决冲突的方法是遍历所有槽位没找到代表删除,或找到一个空槽位插入,探查槽位的次数取决于负载因子和元素均匀分布程度,在这里负载因子<=1,它是一一对应的,要探查槽位的个数是 1/(1-a),a是负载系数,探查方法分线性探查和双散列,两种方法都是用hash取余并通过参数来进行相关运算,只不过双散列取余两次,较之于线性探查能更均匀的分布数据,但是必须限制槽位个数,也就是m的值,目的是为了保证第二次访问任何一个槽位之前其他所有槽位都访问了一遍,复杂度是删除是O(m),,其余是O(1)。最不好的性能是所有槽位都满了,这样查找的复杂度就是O(m),所以我们通常不会让元素个数超过标容量的80%,同样的,如果分布均匀,就是O(1)

补充:链式哈希表中的哈希编码实际是哈希编码对表取模后的值,为了确保哈希编码不会处于表的末尾,而开地址哈希双散列取模可能超出表的边界,就算每个辅助哈希产生的哈希编码都不超过表的边界,但两个相加也许会超过表的边界



二叉树:

树有唯一的父节点,和多个子节点,子节点个数取决于树的类型,二叉树用得比较多,

二叉树结点:根节点,父节点,祖先节点,子节点,叶子节点,后代节点,兄弟节点,树的分支。二叉树的每一个节点都包含3个部分,一个数据成员和两个左右指针,每个节点的左右指针分别指向该节点的子节点。树的周游算法:先序遍历(根节点-左子节点 右子节点) 中序遍历(左子节点 根节点 右子节点) 后序遍历(左子节点 右子节点 根节点) 层级遍历(根 下层从左到右)。树的平衡,保证树的高度尽可能短的过程,如果满足树的所有叶子节点都在最后两层上,且倒数第二层是满的,泽这棵树是平衡的

操作有,插入和删除不同类型的节点,节点个数统计,节点数据获取等

表达式树,节点包含两种类型的对象:操作符和终止值,表示形式有前中后缀三种,遍历方式有前序【根节点开始 中-左-右】、中序【叶节点开始 左-中-右】和后序【叶节点开始 左-右-中】,通过栈来实现,压入后弹出并计算,然后再压入,如此反复。

二叉搜索树,遍历的方法是,当查找的终节点大于它的时候,顺着右子树查找,终点值小于目标节点,顺组左子树查找,从根节点往下,达到分支尽头的时候执行插入操作,终点值不能重复,二叉搜索树的复杂度是O(lg n),因为它遍历一个分支的数据就可以了,但如果树的高度过高,那么树就越来越不平衡,查找一个节点的复杂度就变成了O(n),那么就需要平衡树,平衡标准是右子树高度-左子树高度,如果都是0 +1(左倾斜) -1(右倾斜),那么就是平衡的,这就是树的旋转(AVL树),

旋转方法有LL RR LR RL,其中LR或RL需要考虑三种情况,旋转节点的平衡因子(0 +1 -1),实现二叉搜索树需要节点中的数据data,hidden来标识是否已移除的节点,factor是节点的平衡因子,增删改的操作中,insert是插入节点(balance设置ewing0,如果平衡了就设置为1),hidden是辅助判断的(惰性移除,如果是替换就改变hidden的标号位,不用再平衡),平衡就是switch操作的四种平衡方法,

通过指针能按先后访问所有节点而不用按照一定的搜索顺序遍历,保存指针也会有一些开销;二叉树和链表只有一个保存析构函数,如果不是动态分配的空间,两个都需要NULL;移除节点只能用后序,因为子节点必须在父节点删除前就全部删掉;二叉树中寻找最小和最大节点的复杂度最大是O(n),n是节点个数;如果给定节点个数,那么粉质因子大能保持这棵树的高度较低,平衡性较好,比如B树,减少了遍历层级,磁盘I/O就变小了;二叉搜索树中寻找某个节点x的后继节点,首选找到x,然后移动到右节点,然后不断寻找左子节点,直到边缘也就是叶子节点为止;右子树的左节点,要大于根节点小于父节点,左子树的右节点要大小于根节点,大于父节点,依次类推

其他的树有K叉树,红黑树,Trie树和B树



堆和优先队列

堆是一种树形组织,能迅速确定最大/小值的节点,维持一棵树的代价低于维持一个有序数据集的代价,常用的有排序、任务调度、包裹分拣、霍夫曼编码和负载均衡

堆分为最大值堆和最小值堆,兄弟节点之间没有顺序关系,它是局部有序的。堆是左平衡的树,通过水平遍历方式连续存储到一个数组中,比如处于i位置的一个节点,左节点和右节点位置分别为2i+1和2i+2,父节点是(i-1)/2的取整,最后一个节点的位置是最深层最右端的节点。左平衡树能用这种方法把元素存在数组中,这样数组的空间是连续的,不然如果是存在右边,就会有很大的地址空间是空的,会使空间不连续,浪费空间

heap_insert插入堆的节点计算方法是,将新节点插入末尾,如果是一棵最大值树,就比较父节点大小,逐级向上比较,复杂度是O(lg n),通过把新加入的节点往根部推,就避免了O(nlgn)的复杂度,能降低到O(n)

heap_extract ,找到要释放的节点,用最后一个节点(最深最右边的)代替它,然后排序

heap_parent、heap_left、heap_right不能像上面的增删一样放在公共接口中,因为定位父节点和左右节点的操作不是公开的,也就是说开发人员不允许随意遍历堆的节点

优先队列也是根据堆来实现的,将数据按照优先顺序排序,相比复杂度为O(n)的两两比较,用堆来实现就能把复杂度降低到O(lgn),堆顶部的节点优先级最高,包裹分拣就是通过插入和获取节点的优先级来实现,当然了,对于低优先级的包裹,如果总有高优先级插入会出现饿死的现象,应该依据时长来提高低优先级的优先级别,使它最终能成为高优先级,



图的元素是顶点和边,分有向和无向,两个重要关系是邻接和关联,邻接是两个顶点之间的关系,有向图中的邻接分指向,无向图中的邻接是对称的,关联是顶点和边之间的关系,有出度和入度。路径是顶点遍历的轨迹,没重复顶点的路径成为简单路径是指路径包含相同顶点两次或两次以上,有向无环图称为DAG

如果每个顶点都能通过某条路径到达其他顶点,那么我们称它是连通的,如果在有向图中就是强连通,如果只有部分是连通的就是(强)联通分支,如果失去某个借点就使得图或某分支失去连通性,就称该结点为关结点,如果使图失去连通性就是桥。没有关节点的连通图称为双连通图,图本身可能不是双连通的,但仍可能包含双连通分支

邻接表,相邻顶点表示法,有向图中,所有邻接表中顶点总数与边的总数相等,无向图中邻接表中的顶点总数是总边数的两倍。邻接表结构链表采用链表是因为增删链表时能动态收缩,采用集合表示邻接表,是因为邻接表包含的顶点是无序的,邻接表结构链表主要操作是找出特定顶点的邻接链表,所以链表更适合             

搜索方法:

广度优先--遍历节点的邻接节点,没遍历到的代表不可达,用于最小生成树和最短路径,记录遍历过的顶点个数,广度搜索会生成最短路径,也可以生成一颗广度优先树用来维护每个顶点的唯一祖先节点信息的数据结构,说白了,广度优先就是寻找多种可能性,可能性穷尽后就涂黑,从前往后

深度优先--递归访问顶点的所有没遍历过的相邻顶点,深度优先也是维护唯一顶点的祖先结构,,每棵树都包含搜索过程中发现的与该顶点唯一相连的节点,说白了,深度优先就是寻找子节点的多种可能性,子节点穷极后回到父节点,依次涂黑,从后往前。记录发现顶点和完成搜索的次数通过记录涂灰和涂黑的次数来实现


复杂度大概是O(v+n),主要关系是顶点和边,还有邻接表、邻接关系、邻接顶点集合等


广度优先,计算网络跳数,start起始顶点,hops代表返回的跳数链表,data是数据指针,color是顶点颜色,

深度优先,拓扑排序,有向图也就是优先级图,start起始顶点,order是排序后的顶点链表,data是数据指针,color是顶点颜色,


建模,互联网--单点故障源,要保证网络中没有关节点;航线结构,广度优先,航线不可用就是不可达,关闭的航线组成了桥;交通灯系统,有向图适合,它的状态是顺序性的,


二叉树,是一种有向无环图,除了根节点,每个节点最多只能是两条边的点,一条边的终点,每个顶点的邻接表都包含自己的子节点




图算法

最小生成树--无向带权图素有顶点连起来,常用的,优化管道 ,通信网络,有线电路板  O(E*V的平方)   //V是顶点,E是条数

最短路径--有向带权图两个顶点最短路径,常用的,路由表,航路选择,交通监控           O(E*V的平方)

旅行商问题--无向带权图中每个顶点遍历一次,并返回起始点,常用的,快递服务,闭合运输系统   O(V的平方)


它们都使用了广度优先和贪心算法(霍夫曼树),就是遍历所有可能性然后选取最(小)值,如果通过优先队列避免探寻所有点,能极大提高效率,复杂度降低为O(ElgV)。

最小生成树能够保证整个拓扑图的所有路径之和最小,但不能保证任意两点之间是最短路径。用贪婪算法来实现全局最小。实现算法:Prims算法和Kruskal算法。
最短路径是从一点出发,到达目的地的路径最小。可以用动态规划来实现。实现算法:dijkstra算法

说白了,一点到其余各点的路径和最小,就是一点到其它点的最短路径和。最短路径是某个点到其他点的,有基础点,最小生成树,是两两点之间最小,无基础点

最短路径相比最小生成树,采用的是累加权值,选取权值最小的路径,最小生成树是寻找父子节点的最小值,并不断用比它还小的值替代,旅行商是计算所有顶点的最短距离,最后的顶点与初始顶点连接形成闭环,也就是哈密顿圈;数据结构上存着数据、颜色、边的权值和父顶点以及路径值等信息



排序和搜索

排序分比较排序和线性时间排序,搜索包括线性搜索和二分法查找

插入排序复杂度是O(n的平方),遍历一个数据集的同事遍历另外一个,在无序集中遍历数据并和有序集中的数据比较后插入,外部循环时间是n-1,内部循环时间最大是从右到左遍历所有元素,复杂度是(n(n+1))/2 -n

快速排序是通过中位数法选取分割值,将数据分成两部分,然后对两部分继续递归调用快排,最终只剩一个元素的时候排序完成,复杂度是O(nlgn),数据集不断二分,同事遍历每个数据集,常用例子有目录列表 ls

归并排序,思想和快排相同,只是不用分割,把数据等分,然后递归调用归并排序,最后合并,效率较高,但是需要额外的存储空间,是快排的两倍,适合大数据,复杂度是O(nlgn)

计数排序是线性排序,利用数组索引来记录元素出现的次数,复杂度是O(n+k),n是要排序的元素个数,k是data中最大的整数+1,具体实现是记录次数后,后一个依次加上前一个出现的次数,作为偏移量,从末尾开始查找数据并按照偏移量放入新开的空间中,每次减一

基数排序,从数据最低有效位到最高有效位进行比较,是线性算法,复杂度是pn+pk,p是每个元素的位数,n是元素的个数,k是基数,其实就是按位应用计数排序,只不过基数排序每次仅考虑元素的一个位,而计数排序需要告诉函数具体哪一位要处理而且还要告诉如何获取每一位的值,通过幂运算和模运算来获取每位的值。相比于快速排序,如果复杂度中,k的 值与n不接近或大于n,那么它的效率远不如快速排序

二分法查找,能够应用于任何类型的数据,依赖于有序集合,适合静态数据,因为有序数据集合的增删代价高,查询代价更高,复杂度是O(mlgn),n是字典中单词数,n是要查的单词数,

海量用归并排序,插入数据用插入排序,元素个数大小固定用基数排序,大型数据用快速排序,有序数据集查找元素继任者用二分查找


--冒泡排序,两两比较,复杂度是O(n的平方),

--堆排序,

--内省排序(类似于快排,检测时会切换到堆排序),

--桶排序用来随机均匀分布对每个桶中的元素排序,

--选择排序是复杂度为O(lgn)的算法,将元素配对,选出一个赢家,放到另一个有序集中



霍夫曼树

1、霍夫曼树,用较少的位对出现频率高的符号编码,用较多的位对出现频率低的符号编码,需要计算符号的频率,然后把频率的低的两两合并,最终合并成一棵树,根节点是符号的总个数,叶子节点是符号和符号的频率,压缩需要计算频率然后压缩,解压比较快,只需要扫描一次霍夫曼树,时间复杂度是O(n)

2、LZ77算法,把长字符串编码成短小的标记,在向前缓冲区中不断寻找能够与字典中短语匹配的最长短语,比霍夫曼耗时,要花很多时间寻找滑动窗口中的匹配短语,但解压速度更快,因为每个标记都标明了字符在缓冲区中的位置,包括滑动窗口偏移量、长度和未匹配符号,复杂度是O(n)。如果所有符号的频率类似且压缩量小那么霍夫曼编码耗时高。

如果符号标记数量多,那么有可能LZ77压缩后更大,可能是由于滑动窗口小,没有有效利用重复词组,滑动窗口的大小,要考虑滑动窗口中进行搜索的时间的长短以及短语标记的空间消耗,还有就是前向缓冲区的大小决定了匹配短语的最大长度,也要平衡它和较长短语所带来的控件增长之间的关系,算法上,提高滑块匹配短语的效率的方法是,用二叉树或哈希表来代替窗口,存储前面遇见的短语

3、对比,LZ77比霍夫曼有更好的压缩比,但压缩耗时较高,适合大型发布软件包,用户只需要解压一次,解压速度很快;如果是数据交互,每次都要压缩和解压,还是用霍夫曼编码,压缩和解压的总体时间更短










0 0
原创粉丝点击