精华内容
下载资源
问答
  • 公众号:前端印象 不定时有送书活动,记得关注~ 关注后回复对应文字领取:【面试题】、【前端必看电子书】、【数据结构与算法完整代码】、【前端技术交流群】 数据结构——哈希表 一、什么是哈希表 二、哈希表的优...

    本系列文章【数据结构与算法】所有完整代码已上传 github,想要完整代码的小伙伴可以直接去那获取,可以的话欢迎点个Star哦~下面放上跳转链接

    数组是我们平时常见的并且经常使用的一种数据结构,那么它具有什么优点呢?我们都知道,在我们知道数组中某元素的下标值时,我们可以通过下标值的方式获取到数组中对应的元素,这种获取元素的速度是非常快的。

    但是呢,数组也是有一定的缺点的,如果我们不知道某个元素的下标值,而只是知道该元素在数组中,这时我们想要获取该元素就只能对数组进行线性查找,即从头开始遍历,这样的效率是非常低的,如果一个长度为10000的数组,我们需要的元素正好在第10000个,那么我们就要对数组遍历10000次,显然这是不合理的。

    所以,为了解决上述数组的不足之处,引入了哈希表的概念,哈希表在很多语言的底层用到的非常的多,本文我们将来讲解一下哈希表。因为哈希表的底层知识很多,但是代码是非常简洁的,所以请大家耐心看完。

    • 公众号:前端印象
    • 不定时有送书活动,记得关注~
    • 关注后回复对应文字领取:【面试题】、【前端必看电子书】、【数据结构与算法完整代码】、【前端技术交流群】

    一、什么是哈希表

    文章开头就说了,哈希表可以弥补数组的一些缺点,所以我们就可以在数组的基础上做一些改动,来实现哈希表。

    我们还是先以一个具体的例子来理解一下哈希表。

    假如图书馆有十万本图书,图书管理员把它们随意地放置在书架上,到数组中就是这个样子的
    在这里插入图片描述
    此时书架上的书是无序的,突然有一个同学说我要来找 《零基础入门JavaScript》 这本书,那么他们只能从书架上的第一本书开始挨个往后找,直到找到这本书为止。

    这就像数组中要找一个不知道下标的元素,只能遍历整个数组,这样不太好。

    那么有人就要说了,那我们在将图书放置到书架上的时候,给每一本书一个下标不就好了吗?到时候找书的时候直接通过下标来找到书,这样岂不是很方便?

    这话说的没错,我们平时去图书馆借书的时候,都是通过计算机来查询到一本书的编号,然后再去指定书架上找书的,那么在你查阅计算机时,计算机难道要遍历整个图书库,然后找到你要的那本书吗?这显然是不可能的,所以我们要研究出一种让计算机拿到一本书的名字时,就能得知这本书的编号的数据结构,这里就引入了哈希表的概念

    为了方便我们了我们先决定一个可接受的数组长度,比如说设置数组长度为10
    在这里插入图片描述
    然后把每本书的书名看作是一个字符串,例如 "零基础入门JavaScript",此时该字符串的长度为15,我们就分别获取每一个字符的Unicode 编码,然后用著名的秦九韶算法来将每一个Unicode 编码组合成一个很大的数。

    这里先来介绍一下 秦九韶算法。秦九韶算法是中国南宋时期的数学家秦九韶提出的一种多项式简化算法,在西方被称作霍纳算法

    这是一个一元n次多项式的求和公式,我们可以看到,在这个公式中,一共有n次加法和(n+1)*n/2次乘法
    在这里插入图片描述
    而秦九韶算法就是对该公式进行了提取公因式,最终将式子变成这样
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    看到最终结果,我们可以看到,这个式子被简化以后,只需要进行n次乘法计算和n次加法计算了。

    这样的简化过程也为计算机处理多项式的求值问题提供了很大的便利,大幅度提高了求值的效率。这也就是我们为何要使用秦九韶算法来将字符串的Unicode 编码组合成一个很大的数的原因了。

    我们通过查阅 零基础入门JavaScript 中每个字符串的Unicode 编码,它们分别为 38646225223078420837383767497118978399114105112116

    然后我们把这些Unicode 编码代入到秦九韶算法中,得到 3.539723283210537e+26,这是一个特别特别特别大的数字,你可能想问,我们获得这么大的数干什么呢?因为我们刚才不是设定了数组的长度为10吗?所以我们将这个得到的数除以10取余,将获得的余数作为该元素在数组中的下标值。

    为什么要这么做呢?因为数组的长度为10,所以数组的下标值范围为0~9,那么一个数除以10取余,那么该余数的范围是不是也是0~9呢?那不正好跟数组的下标值对应起来了吗?
    在这里插入图片描述
    讲到这里了,我再来解释一下,为什么要通过秦九韶算法获取到一个这么大的数,然后再取余得到下标值呢?这不是多此一举么,为何不直接把每一个字符的Unicode 编码相加再直接取余,这样连乘法都不用算,全是加法,计算效率更高。那么我们再来看一个例子,就知道为何要用先获取一个很大的数了:

    首先,第一个字符串是 bcd,每个字符的Unicode 编码分别为 9899100,加起来等于297297 % 10 = 7,所以字符串 bcd 在数组中的下标值就为7;
    第二个字符串是 ace,每个字符的Unicode 编码分别为 9799101,加起来也等于297,所以同样的,ace 在数组中的下标值也同样为7;

    此时,两个字符串的下标值就重复了,这种现象在哈希表中称为冲突,在下文我会详细讲到。话说回来,就是因为简单的相加取余会很容易就出现冲突的现象,所以我们才选择利用秦九韶算法来给每个字符串规定相应的下标值,这样能在很大程度上保证了每个元素的下标值不重复,当然了也是会有一定几率重复的,这是不可避免的。
    在这里插入图片描述
    上面将一个字符串转化为数组长度范围内的下标值的过程就称作为 哈希化,而实现哈希化的代码一般会封装在一个函数里,这个函数就叫做 哈希函数

    按照上述所说的方法,对每本书的书名字符串进行哈希化,就可以使每本书获得一个下标值并存储在数组中了,之后当你要查阅一本书的编码时,你输入那本书的书名,计算机拿到以后,只需要通过哈希函数的处理,就可以获得这本书的下标值,然后瞬间获得这本书的编号。

    相信大家对 哈希表 应该有了一定的了解了,如果没看懂,我们下面再慢慢讲解。

    二、哈希表的优缺点

    在刚才哈希表的描述中,大家一定能看出哈希表的优缺点了,这里我来给大家总结一下吧~

    (1)优点

    首先是哈希表的优点:

    1. 无论数据有多少,处理起来都特别的快
    2. 能够快速地进行 插入修改元素删除元素查找元素 等操作
    3. 代码简单(其实只需要把哈希函数写好,之后的代码就很简单了)

    (2)缺点

    然后再来讲讲哈希表的缺点:

    1. 哈希表中的数据是没有顺序的
    2. 数据不允许重复

    三、冲突

    前面提到了冲突,其含义就是在哈希化以后有几个元素的下标值相同,这就叫做 冲突。 那当两个元素的下标值冲突时,是后一个元素是不是要替换掉前一个元素呢?当然不是!

    那么如何解决冲突这个现象呢?一般是有两种方法,即拉链法(链地址法)开放地址法

    (1)拉链法

    这种方法是很常用的解决冲突的方法。我们还是拿上面那个例子来说,10本图书通过哈希化以后存入到长度为10的数组当中,难免有几本书的下标值是相同的,那么我们可以将这两个下标值相同的元素存入到一个单独的数组中,然后将该数组存放在他们原本所在数组的下标位置,如图
    在这里插入图片描述
    假设图书1图书2 哈希化后的下标值都为3,那么我们就可以在原数组下标3的位置放一个数组,同时存放这两本图书。因此,无论是查询哪本图书,计算机获得的下标值都是3,然后再对这个位置的数组进行遍历即可获得想要的图书。

    这是第一种解决 冲突 的方法,但使用是还是需要考虑数组长度是否合适的,之后会进行讲解。

    (2)开放地址法

    这种方法简单来说就是当元素下标值发生冲突时,寻找空白的位置插入数据。假设当前下标值为1和3的位置分别已经插入了 图书3图书5,这时将 图书6 进行哈希化,发现它的下标值也是1,此时与 图书3 发生冲突,那么此时 图书6 就可以找到下一个空着的没插入元素的位置进行插入,如图
    在这里插入图片描述
    其实当发生冲突时,寻找空白的位置也有三种方法,分别是 线性探测二次探测再哈希法

    1. 线性探测

    顾名思义,线性探测的意思就是,当某两个元素发生冲突时,将当前索引+1,查看该位置是否为空,是的话就插入数据,否则就继续将索引+1,以此类推……直到插入数据位置。

    但这种方法有一个缺点,那就是当数组中连续很长的一个片段都已经插入了数据,此时用线性探测就显得效率没那么高了,因为每次探测的步长都为1,所以这段都已经插入了数据的片段都得进行探测一次,这种现象叫做 聚集。如下图,就是一个典型的聚集现象
    在这里插入图片描述
    图书8 的下标值为1,与 图书3 冲突,然后进行线性探测,依次经过 图书6、5、1、7 都没有发现有空白位置可以插入,直到末尾才找到空白位置插入,这样挺不好的,所以我们可以选用 二次探测 来缓解 聚集 这种现象。

    2. 二次探测

    二次探测 在线性探测的基础上,将每次探测的步长改为了当前下标值 index + 1²index + 2²index + 3² …… 直到找到空白位置插入元素为止

    还是举一个例子来理解一下 二次探测

    假如现在已存入 图书3图书5图书7,如图
    在这里插入图片描述
    然后此时要存入一个 图书6,通过哈希化以后求得的下标值为2,与 图书5 冲突了,所以就从索引2的位置向后再移动 个位置,但此时该位置上已存有数据,如下面这个动图演示
    在这里插入图片描述
    所以此时从索引为2的位置向后移动 个位置,此时发现移动后的位置上也已存有数据,所以仍无法插入数据,如下面这个动图演示
    在这里插入图片描述
    因此,我们继续从索引2的位置向后移动 个位置,此时发现,移动后的位置上有空余位置,于是直接在此插入数据,这样一个二次探测的过程就完成了,如下列动图演示
    在这里插入图片描述
    我们可以看到,二次探测 在一定程度上解决了 线性探测 造成的 聚集 问题,但是它却在另一种程度造成了一种聚集,就比如 …… 上的聚集。所以这种方式还是有点不太好。

    3. 再哈希法

    再哈希法 就是再将我们传入的值进行一次 哈希化,获得一个新的探测步数 step,然后按照这个步数进行探测,找到第一个空着的位置插入数据。这在很大的程度上解决了 聚集 的问题。

    既然要再进行哈希化获得一个探测的步数,那么这个哈希化的处理过程一定要跟第一次哈希化的处理过程不一样,这样才能确认一个合适的搜索步长,提高查找效率。

    这里,我们就不用担心如何写一个不一样的哈希函数了,给大家看一个公认的比较好的哈希函数:step = constant - (key % constant)

    其中,constant 是一个自己定的质数常量,且小于数组的容量; key 就是第一次哈希化得到得值。

    然后我们再通过这个函数算得的步长来进行查找搜索空位置进行插入即可,这里就不做过多的演示了。

    四、哈希表的扩容和减容

    在了解哈希表的扩容之前,我们来了解一个概念,叫做填充因子,它表示的是哈希表中的数据个数与哈希表长度的比值。其决定了哈希表的存取数据所需的时间大小。

    当我们用第一种解决冲突的办法——拉链法,填充因子最小为0,最大为无限大,这是因为该方法是通过在数组中的某个位置插入一个数组用来存储互相冲突的元素,因此,只要有可能,哈希表的长度可以很小,然后数据都存储在内置的数组中,这样填充因子就可以无限大了。

    那当我们用第二种解决冲突的办法——开放地址法,填充因子最小为0,最大只能为1,这是因为开放地址法的实现原理是找哈希表中空位置插入元素,因此哈希表中的数据量不会大于哈希表的长度,从而填充因子最大也只能是1。

    在这里插入图片描述

    把哈希表比作是个教室,如果教室里坐满了人,然后让你从中找出你的朋友,那密密麻麻的,是不是特别不好找,可能会眼花缭乱;但是如果这个教室里只坐了 2/3 或者 1/2 的人,那么人群看起来就没那么密密麻麻,那让你找到你的朋友也许会相对容易一点。

    也正因为这样的情况,我们可以在适当的时候根据填充因子的大小对哈希表的长度进行扩大,从而减小填充因子的大小,降低我们数据存取所耗费的时间。

    这里我们就将填充因子等于 0.75 作为哈希表扩容的临界点,同时会在后面封装哈希表的时候实现扩容

    当然,填充因子太小也是不合适地,所以我们也会在适当的地方添加减容功能,即将填充因子等于 0.25 作为哈希表减容的临界点。

    五、哈希表的方法

    老规矩,我们在封装哈希表之前,先来看看哈希表常见的方法都有哪些

    方法含义
    put()向哈希表中插入数据或修改哈希表中数据
    get()获取哈希表中的某个数据
    del()删除哈希表中某个数据
    isEmpty()判断哈希表是否为空
    size()返回哈希表内元素个数
    resize()改变哈希表容量,并将数据放到正确的位置上
    isPrime()判断某个数是不是质数
    toPrime()获取离某个数最近的质数

    六、用代码实现哈希表

    前提:

    1. 本文选用链地址法解决冲突问题
    2. 涉及到常量的地方,都选用质数,例如哈希表容量 、霍纳算法的常量等。因为在数论上,使用质数可以尽可能地使数据在哈希表中均匀分布

    (1)创建一个构造函数

    首先创建一个大的构造函数,用于存放哈希表的一些属性和方法。

    function HashTable() {
    	// 属性
    	
    	// 用于存储数据
        this.storage = []
        
        // 统计哈希表内数据个数
        this.count = 0
        
        // 设定哈希表初始长度
        this.length = 7
    }
    

    因为我们是通过数组来实现的哈希表,所以设置了属性 storage 来存储数据;然后定义了属性 count 用于统计哈希表内的数据个数,方便之后用于计算填充因子;最后定义了属性 length 设定了哈希表的初始长度为质数7

    (2)封装哈希函数

    在文章的开头,我就用霍纳算法讲解了哈希化的过程,因此我们在封装哈希函数时,就也通过霍纳算法的最终化简结构来实现

    这里,我放上霍纳算法的化简结果,方便大家观看学习

    P(n) = a 0 a_0 a0 + x( a 1 a_1 a1+x( a 2 a_2 a2 +…+ x( a n a_n an − 1 + x a n a_n an) ) )

    我们来看一下代码

    function HashTable() {
    	// 属性
    	
    	// 用于存储数据
        this.storage = []
        
        // 统计哈希表内数据个数
        this.count = 0
        
        // 设定哈希表初始长度
        this.length = 7
    
    	//封装哈希函数
        HashTable.prototype.hashFunc = function (str, size) {
            let hashCode = 0
    
            //取一个很大的数
            for (let i = 0; i < str.length; i ++) {
                hashCode = 37 * hashCode + str.charCodeAt(i)
            }
            
            //取余
            return hashCode % size
        }
    }
    

    这里的霍纳算法中的常数我就随便取了一个质数37,大家也可以随便选别的稍微大一点的质数

    哈希函数接收两个参数,第一个参数为 str ,即我们之后传入数据的 key ;第二个参数为 size ,即哈希表的长度,所以可以直接进行调用我们设定的属性 this.length

    (3)实现put()方法(不具备扩容功能)

    put()方法就是向哈希表插入数据或者修改哈希表中某数据。

    该方法接收两个参数,第一个参数为 key;第二个参数为 value 。即相当于传入一个键值对

    实现思路:

    1. 通过哈希函数,将 key 哈希化,获取一个索引 index
    2. 判断哈希表 storage 数组的索引 index 上有无数据,若无,则直接在该位置上创建一个空数组 arr,并把我们传入的键值对打包放到一个新的数组中存到 arr
    3. 若有数据,则遍历该索引上的数组每个元素,比对每个元素的 key 是否与我们传入的 key 相等,若有查询到相等的值,则用我们传入的 value 替换查询到的该元素中的 value ,这就实现了修改数据的功能
    4. 若没有查询到相等的值,则直接将我们的键值对打包放到一个新的数组中存储到哈希表 index 索引上的数组中去,此时 this.count + 1

    为了方便大家理解,我用动图来给大家演示该方法的实现过程

    首先是插入数据操作
    在这里插入图片描述
    接下来是修改数据操作

    在这里插入图片描述

    我们来看一下代码

    function HashTable() {
    	// 属性
    	
    	// 用于存储数据
        this.storage = []
        
        // 统计哈希表内数据个数
        this.count = 0
        
        // 设定哈希表初始长度
        this.length = 7
    
    	//封装哈希函数
        HashTable.prototype.hashFunc = function (str, size) {
            let hashCode = 0
    
            //取一个很大的数
            for (let i = 0; i < str.length; i ++) {
                hashCode = 37 * hashCode + str.charCodeAt(i)
            }
            
            //取余
            return hashCode % size
        }
    
    	// 插入或修改数据
        HashTable.prototype.put = function (key, value) {
            // 1.哈希化获得下标值
            let index = this.hashFunc(key, this.length)
            let current = this.storage[index]
    
            // 2.判断该下标值的位置是否有数据
            // 2.1无数据
            if(!current) {
                this.storage[index] = [[key, value]]
                this.count ++
                return;
            }
            
    		// 2.2有数据
    		// 3.遍历对应索引上的数组
            for (let i = 0; i < current.length; i ++) {
                // 3.1已存在相同数据
                if(current[i][0] === key) {
                    current[i][1] = value
                    return;
                }
            }
            
            // 3.2未存在相同数据,直接添加数据
            current.push([key, value])
            this.count ++
        }
    }
    

    我们来使用一下该方法

    let ht = new HashTable()
    
    // 执行put()方法6次
    ht.put('abc', '123')
    ht.put('hgf', '124')
    ht.put('wds', '125')
    ht.put('wer', '126')
    ht.put('kgl', '127')
    ht.put('kmg', '128')
    
    // 查看哈希表内数据个数
    console.log(ht.count)              // 6
    
    // 通过storage属性查看一下哈希表的内部结构
    console.log(ht.storage)
    
    /* storage打印结果
    [
      [ [ 'wds', '125' ], [ 'kgl', '127' ], [ 'kmg', '128' ] ],
      [ [ 'wer', '126' ] ],
      <1 empty item>,
      [ [ 'hgf', '124' ] ],
      [ [ 'abc', '123' ] ]
    ]
    */
    

    此时的哈希表内部是这样的
    在这里插入图片描述

    (4)实现get()方法

    get()方法是用于查询哈希表中某个数据。该方法直接收一个参数,即用于查询的 key

    实现思路:

    1. 通过哈希函数,将 key 哈希化,获取一个索引 index
    2. 判断哈希表 storage 数组的索引 index 上有无数据,若无,则返回 false
    3. 若有数据,则遍历该索引上的数组每个元素,比对每个元素的 key 是否与我们传入的 key 相等,若有查询到相等的值,则返回该值的 value
    4. 若无数据,则返回 false

    思路和代码都比较简单,我们直接来看代码

    function HashTable() {
    	// 属性
    	
    	// 用于存储数据
        this.storage = []
        
        // 统计哈希表内数据个数
        this.count = 0
        
        // 设定哈希表初始长度
        this.length = 7
    
    	//封装哈希函数
        HashTable.prototype.hashFunc = function (str, size) {
            let hashCode = 0
    
            //取一个很大的数
            for (let i = 0; i < str.length; i ++) {
                hashCode = 37 * hashCode + str.charCodeAt(i)
            }
            
            //取余
            return hashCode % size
        }
    
    	// 获取数据
        HashTable.prototype.get = function (key) {
            // 1.获取相应的下标值
            let index = this.hashFunc(key, this.length)
            let current = this.storage[index]
    		
    		// 2.判断该下标值的位置是否有数据
            // 2.1 若该下标值位置不存在任何数据,则查找失败
            if(!current) {
                return false
            }
    
            // 2.2 该下标值位置有数据
            // 3. 进行遍历查找
            for(let i in current) {
                // 3.1 找到对应数据并返回value
                if(current[i][0] === key) {
                    return current[i][1]
                }
            }
    
            // 3.2 没有找到对应数据,返回false
            return false
        }
    }
    

    我们来使用以下该方法

    let ht = new HashTable()
    
    // 执行put()方法 6次
    ht.put('abc', '123')
    ht.put('hgf', '124')
    ht.put('wds', '125')
    ht.put('wer', '126')
    ht.put('kgl', '127')
    ht.put('kmg', '128')
    
    // 执行get()方法,获取 key为 'hgf' 的值
    console.log(ht.get('hgf'))           // 124
    

    (5)实现del()方法(不具备减少容量功能)

    del()方法是删除哈希表中某个数据。该方法接收一个参数 key

    实现思路:

    1. 通过哈希函数,将 key 哈希化,获取一个索引 index
    2. 判断哈希表 storage 数组的索引 index 上有无数据,若无,则返回 false ,表示删除失败
    3. 若有数据,则遍历该索引上的数组每个元素,比对每个元素的 key 是否与我们传入的 key 相等,若有查询到相等的值,则直接删除该值,此时 this.count --,并返回被删除元素的 value
    4. 若没有查询到相等的值,则返回 false ,表示删除失败

    我们来看一下代码

    function HashTable() {
    	// 属性
    	
    	// 用于存储数据
        this.storage = []
        
        // 统计哈希表内数据个数
        this.count = 0
        
        // 设定哈希表初始长度
        this.length = 7
    
    	//封装哈希函数
        HashTable.prototype.hashFunc = function (str, size) {
            let hashCode = 0
    
            //取一个很大的数
            for (let i = 0; i < str.length; i ++) {
                hashCode = 37 * hashCode + str.charCodeAt(i)
            }
            
            //取余
            return hashCode % size
        }
    
    	// 删除数据
        HashTable.prototype.del = function (key) {
            // 1.获取相应的下标值
            let index = this.hashFunc(key, this.length)
            let current = this.storage[index]
    		
    		// 2. 判断该索引位置有无数据
            // 2.1 该下标值位置没有数据,返回false,删除失败
            if(!current) {
                return false
            }
    
            // 2.2 该下标值位置有数据
            // 3. 遍历数组查找对应数据
            for (let i in current) {
            	let inner = current[i]
                // 3.1 找到对应数据了,删除该数据
                if(inner[0] === key) {
                    current.splice(i, 1)
                    this.count --
                    return inner[1]
                }
            }
    
            // 3.2 没有找到对应数据,则删除失败,返回false
            return false
        }
    }
    

    我们来使用一下该方法

    let ht = new HashTable()
    
    // 执行put()方法 6次
    ht.put('abc', '123')
    ht.put('hgf', '124')
    ht.put('wds', '125')
    ht.put('wer', '126')
    ht.put('kgl', '127')
    ht.put('kmg', '128')
    
    // 删除 key为 'hgf'的元素
    ht.del('hgf')              // 删除成功,返回 `124`
    // 删除 key为 'ppp'的元素
    ht.del('ppp')              // 删除失败,返回 false
    
    // 查看哈希表内部结构
    console.log(ht.storage)
    /* storage打印结果
    [
      [ [ 'wds', '125' ], [ 'kgl', '127' ], [ 'kmg', '128' ] ],
      [ [ 'wer', '126' ] ],
      <1 empty item>,
      [],
      [ [ 'abc', '123' ] ]
    ]
    */
    

    此时的哈希表内是这样的
    在这里插入图片描述

    (6)实现isEmpty()方法

    isEmpty()方法是用于判断哈希表是否为空。该方法无需传参

    该方法思路比较简单,直接判断属性 count 是否为 0 即可

    我们来看一下代码

    function HashTable() {
    	// 属性
    	
    	// 用于存储数据
        this.storage = []
        
        // 统计哈希表内数据个数
        this.count = 0
        
        // 设定哈希表初始长度
        this.length = 7
    
    	//封装哈希函数
        HashTable.prototype.hashFunc = function (str, size) {
            let hashCode = 0
    
            //取一个很大的数
            for (let i = 0; i < str.length; i ++) {
                hashCode = 37 * hashCode + str.charCodeAt(i)
            }
            
            //取余
            return hashCode % size
        }
    
    	//判断哈希表是否为空
        HashTable.prototype.isEmpty = function () {
        	return this.count === 0
        }
    }
    

    我们来用一下该方法

    let ht = new HashTable()
    
    console.log(ht.isEmpty())      // false,哈希表为空
    
    ht.put('abc', '123')
    
    console.log(ht.isEmpty())      // true,哈希表不为空
    

    (7)实现size()方法

    size()方法就是用于返回哈希表中数据个数。该方法也无需传参

    该方法实现思路也特别简单,直接返回属性 count 即可

    我们来看一下代码

    function HashTable() {
    	// 属性
    	
    	// 用于存储数据
        this.storage = []
        
        // 统计哈希表内数据个数
        this.count = 0
        
        // 设定哈希表初始长度
        this.length = 7
    
    	//封装哈希函数
        HashTable.prototype.hashFunc = function (str, size) {
            let hashCode = 0
    
            //取一个很大的数
            for (let i = 0; i < str.length; i ++) {
                hashCode = 37 * hashCode + str.charCodeAt(i)
            }
            
            //取余
            return hashCode % size
        }
    
    	// 返回哈希表内元素个数
        HashTable.prototype.size = function () {
            return this.count
        }
    }
    

    我们来用一下该方法

    let ht = new HashTable()
    
    console.log(ht.size())      // 0,哈希表内没有数据
    
    ht.put('abc', '123')
    
    console.log(ht.size())      // 1,哈希表内有一条数据
    

    (8)实现resize()方法

    resize()方法就是用来对哈希表的容量进行改变的,当填充因子过大,我们就对其进行扩容;当填充因子较小,我们就增加其容量。该方法接收一个参数 newLength,表示新的哈希表的容量。

    实现思路:

    1. 将原本的属性 storage 赋值给一个新的变量 oldStorage,然后我们创建一个新的空数组赋值给 storage,并将参数 newLength 赋值给属性 length
    2. 遍历 oldStorage,根据哈希表新的容量大小 newLength 将原本哈希表中所有的数据重新哈希化 、插入数据即可

    该方法难以用动图演示,所以大家好好理解一下,我们直接用代码来实现

    function HashTable() {
    	// 属性
    	
    	// 用于存储数据
        this.storage = []
        
        // 统计哈希表内数据个数
        this.count = 0
        
        // 设定哈希表初始长度
        this.length = 7
    
    	//封装哈希函数
        HashTable.prototype.hashFunc = function (str, size) {
            let hashCode = 0
    
            //取一个很大的数
            for (let i = 0; i < str.length; i ++) {
                hashCode = 37 * hashCode + str.charCodeAt(i)
            }
            
            //取余
            return hashCode % size
        }
    
    	//改变哈希表的容量
        HashTable.prototype.resize = function(newLength) {
            // 1.将旧的哈希表赋值给新变量
            let oldStorage = this.storage
            
            // 2.创建新的空数组作为新的哈希表容器
            this.storage = []
            
            // 3.修改哈希表容量
            this.length = newLength
    
            // 4.遍历旧的哈希表
            for(let i = 0; i < oldStorage.length; i++) {
                let box = oldStorage[i]
                // 4.1 某索引位置上没有数据
                if(box === null) {
                    continue;
                }
    
                // 4.2 某索引上有数据
                for(let j = 0; j < box.length; j++) {
                    let inner_box = box[j]
                    // 4.2.1 将数据重新经过哈希化插入到新的哈希表中
                    this.put(inner_box[0], inner_box[1])
                }
            }
        }
        
    }
    

    这里就不对该方法做过多的演示了,后面会在 put()方法 和 del() 方法的改写中用到

    (9)实现isPrime()方法

    isPrime()方法使用于判断某个数是否为质数的,因此也就只需要接收一个数字为参数即可。

    因为我们要实现哈希表的自动扩容与减容,所以在每次容量改变的时候,需要判断新的容量是否为质数,以此来保证之后哈希表中的数据均匀地分布,所以我们还是有必要来封装一下这个方法的。

    在说方法实现思路之前,我们来回顾一下,质数是只能被 1自身 整除,因此我们来看一下数字 16,显然它不是一个质数,那来看看他能被哪些数整除吧

    等于
    11616
    2816
    4416
    8216
    16116

    非常明显地看到,只要一个数能被整除,那么一个数肯定是大于等于该数的算数平方根;另一个数肯定小于等于该数的算数平方根

    因此,我们在判断一个数是否为质数时,只需从 2 开始逐个判断该数能否被整除,一直判断到该数的算数平方根即可

    那么我们来看一下代码怎么实现的吧

    function HashTable() {
    	// 属性
    	
    	// 用于存储数据
        this.storage = []
        
        // 统计哈希表内数据个数
        this.count = 0
        
        // 设定哈希表初始长度
        this.length = 7
    
    	//封装哈希函数
        HashTable.prototype.hashFunc = function (str, size) {
            let hashCode = 0
    
            //取一个很大的数
            for (let i = 0; i < str.length; i ++) {
                hashCode = 37 * hashCode + str.charCodeAt(i)
            }
            
            //取余
            return hashCode % size
        }
    
    	//判断是是否为质数
        HashTable.prototype.isPrime = function(number) {
            // 1.获取算数平方根,并取整
            let sqrt = Math.floor(Math.sqrt(number))
    
            // 2.从2开始遍历到算数平方根
            for(let i = 2; i <= sqrt; i++) {
                // 2.1 能被整除,返回 false
                if(number % i === 0) return false;
            }
            // 2.2 不能被整除,返回 true
            return true
        }
        
    }
    

    我们来简单验证一下该方法

    let ht = new HashTable()
    
    console.log(ht.isPrime(3));        // true
    console.log(ht.isPrime(4));        // false
    console.log(ht.isPrime(16));       // false
    console.log(ht.isPrime(11));       // true
    

    (10)实现toPrime()方法

    toPrime()方法就是用于获取离某个数最近的质数并返回,因此只需要接收一个数字作为参数即可。

    我们在实现扩容或减容时,初始会简单地 乘以2 或者 除以2,很难保证获得的数是质数,所以我们需要封装这样一个方法来将变换后的值变为质数再进行使用。

    实现思路也很简单,就一直 +1 来寻找质数就好了。

    我们来看一下代码

    function HashTable() {
    	// 属性
    	
    	// 用于存储数据
        this.storage = []
        
        // 统计哈希表内数据个数
        this.count = 0
        
        // 设定哈希表初始长度
        this.length = 7
    
    	//封装哈希函数
        HashTable.prototype.hashFunc = function (str, size) {
            let hashCode = 0
    
            //取一个很大的数
            for (let i = 0; i < str.length; i ++) {
                hashCode = 37 * hashCode + str.charCodeAt(i)
            }
            
            //取余
            return hashCode % size
        }
    
    	//获取离某个数最近地质数并返回
        HashTable.prototype.toPrime = function(number) {
    
            while(!this.isPrime(number)) {
            	number ++
            }
            return number
        } 
    }
    

    我们来简单验证一下该方法

    let ht = new HashTable()
    
    console.log(ht.toPrime(3))             // 4
    console.log(ht.toPrime(32));           // 37
    

    (11)给put()方法增加扩容功能

    什么时候需要进行给哈希表扩容呢?那肯定是在哈希表中数据量增加的时候需要考虑扩容的问题,所以我们将在 put() 方法中加上扩容的判断,上文也说到了,填充因子等于 0.75 是我们扩容的临界点,即当填充因子大于 0.75 时,就对哈希表进行扩容

    话不多说,我们直接在上文封装好的 put()方法中进行改写

    实现思路:

    1. this.count ++ 之后,判断填充因子的大小,即 this.count / this.length 是否大于 0.75,若小于 0.75,则不做任何处理
    2. 若大于 0.75,则先获取一个原来哈希表容量两倍的数 number,再调用 this.toPrime 方法获得一个离 number 最近的一个质数 prime
    3. 最后调用 this.resize 方法,并将 prime 作为参数传入,完成扩容功能

    直接来看代码吧

    function HashTable() {
    	// 属性
    	
    	// 用于存储数据
        this.storage = []
        
        // 统计哈希表内数据个数
        this.count = 0
        
        // 设定哈希表初始长度
        this.length = 7
    
    	//封装哈希函数
        HashTable.prototype.hashFunc = function (str, size) {
            let hashCode = 0
    
            //取一个很大的数
            for (let i = 0; i < str.length; i ++) {
                hashCode = 37 * hashCode + str.charCodeAt(i)
            }
            
            //取余
            return hashCode % size
        }
    
    	// 插入或修改数据
        HashTable.prototype.put = function (key, value) {
            // 1.哈希化获得下标值
            let index = this.hashFunc(key, this.length)
            let current = this.storage[index]
    
            // 2.判断该下标值的位置是否有数据
            // 2.1无数据
            if(!current) {
                this.storage[index] = [[key, value]]
                this.count ++
                return;
            }
            
    		// 2.2有数据
    		// 3.遍历对应索引上的数组
            for (let i = 0; i < current.length; i ++) {
                // 3.1已存在相同数据
                if(current[i][0] === key) {
                    current[i][1] = value
                    return;
                }
            }
            
            // 3.2未存在相同数据,直接添加数据
            current.push([key, value])
            this.count ++
    
    		// 4.判断是否需要进行扩容
            if(this.count / this.length >= 0.75) {
                // 4.1 将哈希表容量扩大一倍
                let newLength = this.length * 2
                // 4.2 获取质数容量
                newLength = this.toPrime(newLength)
                // 4.3 扩容
                this.resize(newLength)
            }
        }
    }
    

    (12)给del()方法增加减容功能

    同样的,在我们的 del() 方法中会涉及到数据减少的情况,即 this.count --,那么此时我们就需要考虑哈希表是否需要减少容量,也就是填充因子是否小于 0.25,若小于并且哈希表容量大于7,则进行减容;否则不做处理

    这里说一下为什么哈希表容量要大于7,因为在减容时,我们要将容量除以2,但哈希表的容量不方便太小太小,所以我就自己设定了一个容量的下限值为7,意思就是当哈希表容量小于或等于7时,即使填充因子小于0.25,也无需进行减容

    实现思路:

    1. this.count -- 之后,判断填充因子的大小,即 this.count / this.length 是小于 0.25,若大于 0.25,则不做任何处理
    2. 若小于 0.25 并且哈希表容量大于 7,则先获取一个原来哈希表容量一半的数 number,再调用 this.toPrime 方法获得一个离 number 最近的一个质数 prime
    3. 最后调用 this.resize 方法,并将 prime 作为参数传入,完成减容功能

    我们直接来看代码

    function HashTable() {
    	// 属性
    	
    	// 用于存储数据
        this.storage = []
        
        // 统计哈希表内数据个数
        this.count = 0
        
        // 设定哈希表初始长度
        this.length = 7
    
    	//封装哈希函数
        HashTable.prototype.hashFunc = function (str, size) {
            let hashCode = 0
    
            //取一个很大的数
            for (let i = 0; i < str.length; i ++) {
                hashCode = 37 * hashCode + str.charCodeAt(i)
            }
            
            //取余
            return hashCode % size
        }
    
    	// 删除数据
        HashTable.prototype.del = function (key) {
            // 1.获取相应的下标值
            let index = this.hashFunc(key, this.length)
            let current = this.storage[index]
    		
    		// 2. 判断该索引位置有无数据
            // 2.1 该下标值位置没有数据,返回false,删除失败
            if(!current) {
                return false
            }
    
            // 2.2 该下标值位置有数据
            // 3. 遍历数组查找对应数据
            for (let i in current) {
            	let inner = current[i]
                // 3.1 找到对应数据了,删除该数据
                if(inner[0] === key) {
                    current.splice(i, 1)
                    this.count --
    
    				// 判断是否需要减容
                    if(this.count / this.length < 0.25  && this.length > 7) {
                        // 将哈希表容量减小一倍
                        let number = Math.floor(this.length / 2)
                        
                        // 获取质数容量
                        number = this.toPrime(number)
                        
                        // 减容
                        this.resize(number)
                    }
    
                    return inner[1]
                }
            }
    
            // 3.2 没有找到对应数据,则删除失败,返回false
            return false
        }
    }
    

    七、结束语

    哈希表的讲解就到这里了,希望大家对哈希表有了更深一层的理解。下一篇文章我将讲解一下树结构

    大家可以关注我,之后我还会一直更新别的数据结构与算法的文章来供大家学习,并且我会把这些文章放到【数据结构与算法】这个专栏里,供大家学习使用。

    然后大家可以关注一下我的微信公众号:前端印象,等这个专栏的文章完结以后,我会把每种数据结构和算法的笔记放到公众号上,大家可以去那获取。

    或者也可以去我的github上获取,欢迎大家点个Star

    我是Lpyexplore,创作不易,喜欢的加个关注,点个收藏,给个赞~ 带你们在Python爬虫的过程中学习Web前端

    展开全文
  • 山东省学考算法与程序设计试题选择题1、下列VB表达式中:⑴Sqr(x) ⑵Text1.text ⑶Command1.caption ⑷"45"+"34" ⑸45+34值为字符串类型的是()A⑴⑵⑶ B⑵⑶⑷ C ⑴⑶⑸ D⑵⑷⑸2、如果给出三条线段的长分别为a、b、...

    山东省学考算法与程序设计试题

    选择题

    1、下列VB表达式中:

    ⑴Sqr(x) ⑵Text1.text ⑶Command1.caption ⑷"45"+"34" ⑸45+34值为字符串类型的是()

    A⑴⑵⑶ B⑵⑶⑷ C ⑴⑶⑸ D⑵⑷⑸

    2、如果给出三条线段的长分别为a、b、c,且已知a≤b≤c,要问这三条线段能否构成三角形,仅需下列选项中的哪个判定条件即可?()

    A 其他选项都不对

    B a+c>b

    C a+b>c

    D b+c>a

    3、VB程序中“Dim n As Integer”这条语句的作用是()

    A 定义一个事件过程

    B 定义一个数据输入方法

    C 定义一个变量

    D 定义一个数据处理方法

    4、关于算法的描述,下列选项中正确的是()

    A 算法的每一步骤必须有确切的含义

    B 算法必须有输入

    C 算法的步骤可以是无穷的

    D 算法本身就是一种程序设计语言

    5、关于算法的描述,正确的是()

    A同一种算法只能用一种程序语言实现

    B算法就是数值计算的方法

    C描述算法的方法只有流程图

    D算法是描述解决问题的方法和步骤

    6、算法的描述方法有多种,下列选项中不适合描述算法的是()

    A机器语言 B自然语言 C流程图 D伪代码

    7、长度分别为a、b、c的三条线段,能够组成三角形的条件是()

    A a+b>c Or a+c>b Or b+c>a

    B a+b>c or a+c>b And b+c>a

    C a+b>c Or a+c>b And b+c>a

    D a+b>c And a+c>b And b+c>a

    8db9b291efb95e9d679aca58e4376129.png

    8、已知海伦公式:p=1

    2

    (a+b+c),a、b、c分别为三角形的三条

    边长。利用海伦公式求三角形面积的算法属于()

    A 排序法

    B 解析法

    C 穷举法

    D 查找法

    9、以下程序段中循环体执行的次数是()

    s=0

    i=0

    Do While s<10

    i=i+1

    s=s+i*i

    Loop

    A 1

    B 3

    C 2

    D 4

    10、下列VB表达式中,能正确表达不等式方程|x|>1的解的是()

    A x>-1 and x<1

    B x>-1 or x<1

    C x1

    D x1

    11、一元二次方程ax2+bx+c=0(a≠0)的两个实数根分别为:

    657aa9719b2a6e5f99c9dd43d1dc1224.png

    789e2adb0a495c312bd97f25207c358e.png

    x

    12

    下列表达式正确的是()

    A x

    2=-b-sqr(b^2-4*a*c)/(2*a) B x

    1

    =(-b+sqr(b^2-4ac))/(2*a)

    展开全文
  • cpu都有什么调度算法 什么是CPU调度? (What is CPU Scheduling?) CPU scheduling is a process which allows one process to use the CPU while the execution of another process is on hold(in waiting state) ...

    cpu都有什么调度算法

    CPU scheduling is a process which allows one process to use the CPU while the execution of another process is on hold(in waiting state) due to unavailability of any resource like I/O etc, thereby making full use of CPU. The aim of CPU scheduling is to make the system efficient, fast and fair.

    CPU调度是一种进程,由于诸如I / O等任何资源的不可用,该进程允许一个进程使用CPU,而另一个进程的执行处于暂停状态(处于等待状态),从而充分利用CPU。 CPU调度的目的是使系统高效,快速且公平。

    Whenever the CPU becomes idle, the operating system must select one of the processes in the ready queue to be executed. The selection process is carried out by the short-term scheduler (or CPU scheduler). The scheduler selects from among the processes in memory that are ready to execute, and allocates the CPU to one of them.

    每当CPU空闲时,操作系统都必须在就绪队列中选择要执行的进程之一。 选择过程由短期调度程序(或CPU调度程序)执行。 调度程序从内存中准备执行的进程中进行选择,并将CPU分配给其中一个。

    CPU调度:调度程序 (CPU Scheduling: Dispatcher)

    Another component involved in the CPU scheduling function is the Dispatcher. The dispatcher is the module that gives control of the CPU to the process selected by the short-term scheduler. This function involves:

    CPU调度功能中涉及的另一个组件是Dispatcher 。 调度程序是使CPU控制由短期调度程序选择的进程的模块。 该功能涉及:

    • Switching context

      切换上下文

    • Switching to user mode

      切换到用户模式

    • Jumping to the proper location in the user program to restart that program from where it left last time.

      跳到用户程序中的正确位置以从上次离开的位置重新启动该程序。

    The dispatcher should be as fast as possible, given that it is invoked during every process switch. The time taken by the dispatcher to stop one process and start another process is known as the Dispatch Latency. Dispatch Latency can be explained using the below figure:

    鉴于在每个过程切换期间都会调用调度程序,因此调度程序应尽可能快。 调度程序停止一个进程并启动另一个进程所花费的时间称为“ 调度延迟” 。 可以使用下图解释调度延迟。

    Dispatch latency of Process Dispatcher

    CPU调度的类型 (Types of CPU Scheduling)

    CPU scheduling decisions may take place under the following four circumstances:

    CPU调度决策可能在以下四种情况下发生:

    1. When a process switches from the running state to the waiting state(for I/O request or invocation of wait for the termination of one of the child processes).

      当进程从运行状态切换到等待状态时(用于I / O请求或调用等待以终止子进程之一)。

    2. When a process switches from the running state to the ready state (for example, when an interrupt occurs).

      当进程从运行状态切换到就绪状态时(例如,发生中断时)。

    3. When a process switches from the waiting state to the ready state(for example, completion of I/O).

      当进程从等待状态切换到就绪状态时(例如,I / O完成)。

    4. When a process terminates.

      当进程终止时

    In circumstances 1 and 4, there is no choice in terms of scheduling. A new process(if one exists in the ready queue) must be selected for execution. There is a choice, however in circumstances 2 and 3.

    在情况1和4中,就调度而言别无选择。 必须选择一个新进程(如果就绪队列中存在一个进程)来执行。 但是,在情况2和3下可以选择。

    When Scheduling takes place only under circumstances 1 and 4, we say the scheduling scheme is non-preemptive; otherwise the scheduling scheme is preemptive.

    当仅在情况1和4下进行调度时,我们说调度方案是非抢占式 ; 否则,调度方案是抢占式的

    非抢占式调度 (Non-Preemptive Scheduling)

    Under non-preemptive scheduling, once the CPU has been allocated to a process, the process keeps the CPU until it releases the CPU either by terminating or by switching to the waiting state.

    在非抢占式调度下,一旦将CPU分配给某个进程,该进程将保留CPU,直到通过终止或切换到等待状态释放CPU。

    This scheduling method is used by the Microsoft Windows 3.1 and by the Apple Macintosh operating systems.

    Microsoft Windows 3.1和Apple Macintosh操作系统使用此调度方法。

    It is the only method that can be used on certain hardware platforms, because It does not require the special hardware(for example: a timer) needed for preemptive scheduling.

    这是可以在某些硬件平台上使用的唯一方法,因为它不需要抢先式调度所需的特殊硬件(例如:计时器)。

    抢占式调度 (Preemptive Scheduling)

    In this type of Scheduling, the tasks are usually assigned with priorities. At times it is necessary to run a certain task that has a higher priority before another task although it is running. Therefore, the running task is interrupted for some time and resumed later when the priority task has finished its execution.

    在这种类型的计划中,通常为任务分配优先级。 有时有必要在运行另一个任务之前先运行优先级更高的某个任务。 因此,正在运行的任务会中断一段时间,并在优先任务完成执行后再继续执行。

    CPU调度:调度标准 (CPU Scheduling: Scheduling Criteria)

    There are many different criterias to check when considering the "best" scheduling algorithm, they are:

    在考虑“最佳”调度算法时,有许多不同的标准要检查,它们是:

    CPU利用率 (CPU Utilization)

    To make out the best use of CPU and not to waste any CPU cycle, CPU would be working most of the time(Ideally 100% of the time). Considering a real system, CPU usage should range from 40% (lightly loaded) to 90% (heavily loaded.)

    为了充分利用CPU而不浪费任何CPU周期,CPU大部分时间都处于工作状态(理想情况下为100%的时间)。 考虑到实际系统,CPU使用率应介于40%(轻载)到90%(重载)之间。

    通量 (Throughput)

    It is the total number of processes completed per unit time or rather say total amount of work done in a unit of time. This may range from 10/second to 1/hour depending on the specific processes.

    它是每单位时间完成的过程总数,或者说单位时间内完成的工作总量。 根据特定的过程,范围可能从10 /秒到1 /小时。

    周转时间 (Turnaround Time)

    It is the amount of time taken to execute a particular process, i.e. The interval from time of submission of the process to the time of completion of the process(Wall clock time).

    它是执行特定过程所花费的时间,即从提交过程到完成过程的时间(挂钟时间)。

    等待的时间 (Waiting Time)

    The sum of the periods spent waiting in the ready queue amount of time a process has been waiting in the ready queue to acquire get control on the CPU.

    在就绪队列中等待一个进程已在就绪队列中等待以获取CPU上的控制权所花费的时间总和。

    平均负载 (Load Average)

    It is the average number of processes residing in the ready queue waiting for their turn to get into the CPU.

    它是就绪队列中等待轮流进入CPU的平均进程数。

    响应时间 (Response Time)

    Amount of time it takes from when a request was submitted until the first response is produced. Remember, it is the time till the first response and not the completion of process execution(final response).

    从提交请求到生成第一个响应所花费的时间。 请记住,这是直到第一个响应而不是流程执行(最终响应)完成的时间。

    In general CPU utilization and Throughput are maximized and other factors are reduced for proper optimization.

    通常,CPU利用率和吞吐量会最大化,而其他因素也会减少,以进行适当的优化。

    调度算法 (Scheduling Algorithms)

    To decide which process to execute first and which process to execute last to achieve maximum CPU utilisation, computer scientists have defined some algorithms, they are:

    为了确定首先执行哪个进程以及最后执行哪个进程以最大程度地利用CPU,计算机科学家定义了一些算法,它们是:

    1. First Come First Serve(FCFS) Scheduling

      先来先服务(FCFS)调度

    2. Shortest-Job-First(SJF) Scheduling

      最短工作优先(SJF)调度

    3. Priority Scheduling

      优先排程

    4. Round Robin(RR) Scheduling

      循环调度(RR)调度

    5. Multilevel Queue Scheduling

      多级队列调度

    6. Multilevel Feedback Queue Scheduling

      多级反馈队列调度

    We will be discussing all the scheduling algorithms, one by one, in detail in the next tutorials.

    在接下来的教程中,我们将详细讨论所有调度算法。

    翻译自: https://www.studytonight.com/operating-system/cpu-scheduling

    cpu都有什么调度算法

    展开全文
  • 算法

    千次阅读 2013-12-12 16:51:34
    算法 译自From Wikipedia, the free encyclopedia   一种计算在位置名为A和B的两个数字a 和b的最大公约数 (g.c.d.) 的算法(欧几里得的算法)的流程图。  该算法通过在两个循环中连续减进行的:如果测试B≥A产生...

    算法

    译自From Wikipedia, the free encyclopedia

     

    一种计算在位置名为A和B的两个数字a 和b的最大公约数 (g.c.d.) 的算法(欧几里得的算法)的流程图。

            该算法通过在两个循环中连续减进行的:如果测试B≥A产生的是"是"(或真)(更准确地说位置B的数字b是大于或等于位置 A的数字a)那么,算法指定B←BA(意思是数字ba替换旧b)。同样,如A>B,那么A←AB。当B(的内容)为0时进程终止,在A生成最大公约数(按照Scott 2009:13派生的算法;符号和绘图风格按照Tausworthe1977)。

            在数学和计算机科学中,算法是一个计算的分步过程。算法用于计算、数据处理和自动推理。

            算法是一种表达成定义明确的有限指令表的一个函数的计算的有效方法。从初始状态和初始输入(可能为空)开始,指令描述当执行一个计算时通过有限数量的明确定义的连续状态,最终产生"输出"并在最终的结束状态终止。从一种状态过渡到下一步并不是一定确定的;一些称为随机算法的算法,将随机输入纳入。

            尽管阿尔·克沃理滋米(al Khwārizmī)的算法提到使用印度-阿拉伯数字进行算术的规则及线性和二次方程的系统解决方案,但成为现代算法的一部分的公式化始于1928年试图解决大卫·希尔伯特(David Hilbert构成的"决策问题" (Entscheidungsproblem)。随后的公式化被构架成试图定义"有效的计算"或"有效的方法";这些公式化包括1930年、1934年和1935年的高德尔-赫尔布兰德-克林(Gödel–Herbrand–Kleene)的递归函数,1936年的阿隆索·丘尔奇(Alonzo Churchλ演算、1936年的爱米尔·泊思特(Emil Post)的"公式1"和1936–7和1939年的阿兰·图灵的图灵机。给予算法一个正式的定义,对应的直观的概念仍然是一个具有挑战性的问题。

    1  非正式定义

            围绕算法的定义的各种观点的详细介绍,请参考算法特征化。以详细方式指定的简单的加法算法的示例在算法的特征化中描述的有,请参见算法的例子

            虽然没有普遍接受的"算法"的正式定义,但非正式的定义可能是"精确地定义一个序列的一组规则",其中会包括所有的计算机程序,包括不执行数值计算的程序。对一些人来说,程序只是一种算法,如果它最终停止的话。对另一些人来讲,程序只是一种算法,如果它执行了许多计算步骤的话。

            算法的一个典型的例子是欧几里得的确定两个整数的最大公约数的算法;一个(有其它的)由上述流程图描述的示例并在后节中作为例子。

            布劳思&杰弗里(Boolos& Jeffrey1974年、1999年)在以下引文中提供字的一个非正式的意思:

            不会有任何人用一些记数法可以写得足够快或足够长或足够小†(†"无限的越来越小......你会试图在分子、原子、电子上写")来列出可数的无限集的所有成员,写出它们的名字,一个接一个。但人可以做点同样有用的某些事,在某些可数无限集的情况下:他们可以给显式指令来确定集的任意有限的n的第n个成员。这种指令是非常明确地给予的,以一种它们可以被计算机或被一个能够履行对符号进行非常初级操作的人遵循的形式。

            "可数的无限"一词意味着"用整数或许能扩展到无穷大的数数"。因此,布劳思&杰弗里说算法意味着从一个任意的"输入"整数或数个整数,理论上可以从0到无穷大选择来"创造"输出整数的过程。因而算法可以是如y=m+n------两个任意"输入变量"m和n产生输出y的代数方程。但众多的作者试图界定表明这个词意味着比这更多的东西的概念,关于次序(对求和的例):

           为指定的"计算机"(机器或人类,配有必要的内部信息和能力)搜索、解码然后处理任意输入的整数/符号m和n、符号+和=...并在"合理的"时间内"有效地"以指定的格式在指定的地点产生输出整数y制定的"移动"的快速、高效、"良好"的进程的精确指令("计算机"理解的语言)。

            算法的概念也用于定义可判定性的概念。这一概念是解释正式系统怎样从一小套的公理和规则开始进入的的中心。在逻辑中,算法需要完成的时间不能测量,因为它显然不与我们习惯的物理维度相关。从这种不确定性上特征化进行着工作,阻止既适合具体(在某种意义上)又适合这一术语的抽象用法的算法的定义的不可利用性。

    2 公式化

            算法是计算机处理数据的必不可少的方式。许多计算机程序包含计算机应执行(按特定的顺序)的详细的特别的指定任务的算法,如计算雇员的薪水或打印学生的报告卡。因此,算法可以被认为是一种任何能通过完备图灵系统模拟的操作序列。断言这个的论文作者包括明斯基(Minsky,1967)、萨瓦奇(Savage ,1987年)和古列维奇(Gurevich,2000年):

            明斯基:"但是,我们也将保持,用图灵......任何能"自然地"被称为是有效的过程其实可以由(简单)的机器实现。虽然这可能看起来极端,但...对其有利的论点很难反驳"。

            古列维奇:"......有利于他的论文的图灵非正式理由判明了更强的论题:每一种算法通过图灵机可以模拟的...

            根据萨瓦奇[1987年],算法是一个由图灵机定义的计算过程"。

            通常,当算法相连于处理信息时,关联数据是从一个输入源中读取、写到输出设备上的,存储进行进一步处理。存储的数据被认为是执行算法的实体的内部状态的一部分。在实践中,状态存储在一个或多个数据结构中。

            对于一些此类计算的过程,必须严格定义算法:指定它适用于所有可能的情况下可能出现的方式。就是任何条件的步骤必须有系统地处理,每种情况的;每个情况的标准必须是明确的(和可计算的)。

            因为算法是精确的确切步骤列表,计算的顺序总是算法运作的关键。指令通常被假定为显式地列出,被描述为"从顶部"开始和进行"到底部",更正式的由控制流描述的想法。

            到目前为止,这种算法的公式化讨论已假定了命令式编程的前提。这是最常见的构想,它会尝试以离散的、"机械的"手段描述一个任务。独有的这个公式化算法的构想是赋值操作,设置变量的值。它源于作为便笺簿的"记忆"的直觉。下面有一个这种赋值的例子。

            对是什么构成一种算法的某些替代构想请参阅功能编程和逻辑编程。

    2.1  算法表示

            算法可以表示成许多种类的符号,包括自然语言、伪代码、流程图、drakon 图(俄罗斯的算法可视化编程语言)、编程语言或控制表 (由编译器处理)来表示。算法的自然语言表达式往往是冗长和含糊不清的,很少用于复杂的或技术的算法。伪代码、流程图、drakon图和控制表是表达算法的结构化方式,避免自然语言语句中很多常见的含糊之处。编程语言主要是以一台计算机能执行的形式表达算法,但常常作为一种定义或文件的算法的方式来用。

            有多种可能的表达和人们可以把给定的图灵机程序表达成机器表(更多看有限状态机、状态转换表、控制表)的一个序列,表达成流程图和drakon-图(更多看状态图),或表达成最基本的机器代码或称为"四节集"的汇编代码(更多看图灵机)。

            算法的表示形式可以分为图灵机说明的三个接受级别:

            1 高级别描述:".......用来描述一种算法的语句,忽略详细信息的实现。在这一级我们不需要陈述这台机器是如何管理其磁带或头的"。

            2 执行描述:".......用来定义图灵机使用它的头和它在其磁带上存储数据的方式的语句。在这一级我们不给出状态或转换功能的详细信息"。

            3公式描述:最详细的"最低级",给予图灵机的"状态表"。

    3  执行

            大多数算法旨在作为计算机程序执行。不过,算法也有其它执行手段,如在生物神经网络(例如,人类的大脑执行算术运算或昆虫寻找食物)、在电气电路或在机械装置中。

    4  计算机算法

    规范的包姆-牙克比尼(Böhm Jacopini)结构的流程图示例:SEQUENCE(序列:页上降序的矩形)、WHILE-DO和IF-THEN-ELSE。三个结构构成原始条件的GOTO (如果测试 = true 然后GOTO到步骤 xxx)(一个钻石),无条件地GOTO(矩形)、各种赋值运算符(矩形)和HALT (停止,矩形)。在分配块内部嵌套这些结构造成复杂的图 (参考Tausworthe 1977:100 114)。

            在计算机系统中,一种算法基本上是编写软件的软件开发人员在程序中写的从给定的输入(可能为null)有效地为要生成输出的目标机预定的"目标"计算机的逻辑的一个实例。

            "优雅"(简洁)的程序、"良好"(快速)的程序:非正式地出现在克努思(Knuth)和精确的柴厅(Chaitin)的"简洁和优雅"的概念:

            克努思:我们要的是某些松散定义的审美意义上的良好算法。一个标准......是执行算法所需的时间的长度。另一个标准是算法对计算机的适应性,其简洁和优雅,等"。

            柴厅:"......一个程序是优雅的,我是说它是生产它的输出的最小可能程序"。柴厅在定义前的前言:"我会让你看你不能证明一个程序是优雅的"------这样的证明应解决停止问题 (同上)。

            算法对算法可计算的函数:对于给定的一个函数可能存在多个算法。这是真的,甚至程序员不用扩大程序员可用的指令集。罗杰斯(Rogers)观察到"这对... 区分算法的概念即过程和算法可计算的函数的概念即映射过程产生的来说是重要的。相同的函数可能有几种不同的算法"。

            不幸的是优良(速度)和优雅(简洁度)之间可能会有权衡的------优雅的程序可能比一个不太优雅的完成一个计算需要更多的步骤。一个使用欧几里德的算法示例显示在下方。

            计算的计算机(计算者)、模型:计算机(或人类"计算者")是一种受限制的类型的机器,一种盲目地跟随它的指令的"离散的确定性的机械装置"。摩尔扎克(Melzak)和兰姆别克(Lambek)的原始模型把这种概念减少到四个要素: (i) 离散的、可区分的位置;(ii) 离散的、难以区分的计数器;(iii) 代理;(iv) 有效的说明代理的能力的指令列表。

            明斯基在他的"可计算性的非常简单的基础"中描述更为合意的兰姆别克的"算盘"模型的变型。明斯基的机器通过其五个指令(或六个,看人怎样计数)按顺序进行除非一个有条件的IF–THEN GOTO或一个无条件地GOTO更改顺序的程序流。除了HALT(停止),明斯基的机器包括三个赋值(替换,代替)操作:ZERO(零) (既位置的内容替换为0:L←0),SUCCESSOR(后续者)(既L←L+1)和DECREMENT(减)(既L←L1)。很少有程序员写这种有限的指令集的"代码"。但明斯基证明(Melzak和Lambek同样)他的机器是只有四种一般指令类型的图灵完整的:有条件的GOTO,无条件的GOTO、分配/更换/替代和停止。

            一种算法的仿真:计算机(计算者)语言:克努思劝告读者"学习算法的最佳方法是尝试它......立即带笔和纸和通过一个例子来"。但关于真实的东西的模拟或执行呢?程序员必须把算法翻译成模拟器/计算机/计算者能够有效地执行的一种语言。思通(Stone)给了一个这样的例子:当计算二次方程根时计算机必须知道怎样取一个平方根。如果它们不的话那么算法必须提供一套能够有效提取一个平方根的规则。

            这意味着程序员必须知道相对于目标计算代理(计算机/计算者)的有效的"语言"。

            但模拟应使用哪种模型呢?范·爱姆德·包阿思(Van Emde Boas)指出"即使我们把复杂性理论基于代替具体的机器的抽象上,选择模型的任意性依然的。此时是仿真的概念进入的时候"。当要测量速度时,指令集要参入。例如,在欧几里德的计算余数的算法子程序中执行将快得多如果程序员有一个可用的"模"(除)指令而不是只是减(或者更糟:只是明斯基的"减")。

            结构化编程、规范的结构:每一丘尔奇-图灵议题由已知的图灵完整机模型任何算法可以计算的,每一明思基的图灵完整性的证明只要求有四个指令类型------有条件GOTO、无条件GOTO、assignment(分配)和HALT(停)。凯梅尼(Kemeny)和库尔茨(Kurtz)观察到在"无纪律"的使用无条件 GOTOs和有条件的IF-THEN GOTOs可导致"意粉代码"时程序员可以使用这些指令写结构化的程序;另一方面"也可能是并不太难的,以一种结构化的语言写糟糕的结构化程序"。陶思沃瑟(Tausworthe)补充了三个包姆-牙克比尼规范结构:SEQUENCE, IF-THEN-ELSE, 和WHILE-DO,另有两个:DO-WHILE和CASE。结构化的另一个好处是它本身是使用数学归纳法的正确性的证明。

            规范流程图符号:称为流程图的图形助手提供一种算法(一种计算机的程序)的描述和文档的方法。像明斯基机器的程序流一样,流程图总是从一个页面的顶部开始和向下进行。其主要符号只有4个:显示程序流的方向的箭头,矩形(SEQUENCE, GOTO)、钻石(IF-THEN-ELSE)和点(OR并列)。包姆-牙克比尼规范结构是由这些原始形状构成的。分结构可以"住在"矩形中但其必须是一个上层建筑发生的退出(exit)。符号和使用它们生成的规范结构在图中显示。

    5  算法例子

            随机值的数组的快速排序算法的动画。

            红色棒标记数据中心元素;在动画开始时,右手边最远的元素选择作为数据中心。一个最简单的算法是找到列表数字(未排序)中的最大数。解决方案需要一定看列表中的每个数,但每个只一次。此后跟随一个简单的算法,可以用高级别描述英语文句陈述:

    5.1  高级别描述:

            1假设第一项最大。

            2 看看列表中每个剩余的项,如果它大于到目前为止的最大项,记下它。

            3 当过程完成后列表中标记的最后一个项是最大的项。

            (准-)正式描述: 写文句但更接近于计算机程序的高级别语言,以下是更正式编码的算法的伪代码或洋泾浜的代码:

            Algorithm LargestNumber

              Input: A non-empty list of numbersL.

             Output: The largest number in the listL.

              largest L0

              for eachitemin the list (Length(L)≥1), do

              if theitem >largest, then

              largest ← theitem

             returnlargest

            "←"是"更改成"的简写形式。例如,"最大 ← 项"意味着最大项的值更改成项的值。

            " return "终止算法和输出之后的值。

    5.2  欧几里德算法

            1908年的添加更多细节的希思(T.L. Heath)的欧几里德的算法示例图。

            欧几里德并没有超越第三次测量,没有举例说明数值。尼克玛克思(Nicomachus)给出了49和21的示例:"我从最大的减小的;剩下28;然后我再从中减去这同一的21(这是可能的);剩下7;我从21减去这个,剩下14;从中我再减去7(这是可能的);剩下7,但不能从7减去7"。希思评论说:"最后一句很好奇,但它的意义是明显够的,一样句子关于'在一个同一的数字'结束的意思也是明显够的。(希思1908:300)。

            欧几里德的算法出现在他的元素书VII("基本数字理论)中的命题II。欧几里德带来了一个问题:"鉴于两个数字彼此不能互为质数,来找它们最大的共同度量"。他定义"[应该]一个数字是由众多的单位组成的":一个可数的数,一个不包括0的正整数。来"度量"是放置一个较短的度量长度s(q倍)连续沿更长长度l直到剩余部分r小于较短的长度s。用现代的话说,余数r=lq*s,q是商数或余数r是"模",除后留下的整数小数部分。

            欧几里得的方法要想成功,起始长度必须满足两个要求:(i)长度必须不是0, (ii)减法必须是"恰当的",测试必须保证两个数字中较小的从较大的减去(或者,两个可以相等它们减将产生0)。

            欧几里德的原始证明添加了第三个:两个长度彼此不是互为质数的。欧几里得这样规定使得他可以构造反证法证明这两个数字的共同度量事实是最大的的。而尼克玛克思的算法与欧几里德的是相同的,当数字彼此互为质数时产生它们共同的度量为数字"1"。所以要精确以下是尼克玛克思的算法。

            使用1599和650的欧几里德的算法的图形表达例。

            1599 = 650*2 + 299

            650 = 299*2 + 52

            299 = 52*5 + 39

            52 = 39*1 + 13

            39 = 13*3 + 0

    5.2.1  欧几里德算法的计算机语言

            执行欧几里得的算法只需几个指令类型------一些逻辑测试(有条件GOTO),无条件GOTO、分配(替换)和减法。

            位置由大写字母符号表征,如 S,A等。

            一个位置中的变化量(数字)以小写字母写并与该位置的名称(通常)相关。例如,开始时的位置L 可能包含数字l=3009。

    5.2.2  非优雅的欧几里得算法的程序

            "非优雅"是一个克奴思使用的基于减法的余数-循环替换他的除(或"模"指令)算法版本的翻译。从克奴思1973:24派生的。根据这两个数字"非优雅"可能会比"优雅"用在较少的步骤计算g.c.d.。

            下面的算法构架成克奴思的欧几里得和尼克玛克思的4步版本,但不是用除找其余数,它使用较短的长度s从剩余的长度r连续减直到r小于s。高级别描述以粗体显示,是从克奴思1973:2–4改编的:

            输入:

            1 [在两个地点L和S放表示两个长度的数字l和s]:

            INPUT L, S

            2 [初始化R:使剩余的长度r等于开始/初始/输入长度l]:

            R ← L

            E0: [确保r≥s]

            3 [确保两个数字中较小的在S和较大的在R]:

           IF R > S THEN

            L的内容是较大的数所以跳过交换步骤 4、5和6:

            GOTO step 6

            ELSE

            交换R和S的内容。

            4  L ← R (这第一步是冗余的,但对于后来的讨论非常有用)。

            5 R ← S

            6 S ← L

            E1:[找余数]:直到R中的剩余长度r小于S中的短长度s,重复从R中的剩余长度r减去S中的测量数字s。

            7 IF S > R THEN

             done measuring so

            这样进行测量

            GOTO 10

            ELSE

            measure again,再测量

            8   R ← R S

            9   [Remainder-loop]: [余数-循环]

                GOTO 7.

            E2:[余数是0?]: 或者(i)最后一次测量是确切的和R中的余数是0程序可以停止,或者(ii)该算法必须继续:最后一次测量在R中留有小于S中的测量数。

            10 IF R = 0 THEN

                 done so 这样做了

                 GOTO step 15

                 ELSE

                CONTINUE TO step 11,

            E3:[交换s和r]:欧几里德算法的外壳。使用余数r来衡量以前较小的数字s是多少:;L作为一个临时位置。

            11  L ← R

            12  R ← S

            13  S ← L

            14  [Repeat the measuring process]: 重复测量过程:

                GOTO 7

            OUTPUT:

            15 [Done. S contains the greatest common divisor]:S包含最大公约数

            输出:

            15 [已做。S包含最大公约数]:

               PRINT S

              DONE:

            16 HALT, END, STOP.

    5.2.3  优雅的欧几里得的算法程序

            以下欧几里得算法的版本只需要有6个核心指令做"非优雅"的要求13个做的;更糟糕的是,"非优雅"的需要更多类型的指令。"优雅"的流程图可以在这篇文章的顶部发现。在(非结构化)的Basic语言中步骤有编号,指令LET [] = [] 是用符号化的←分配指令。

              5 REM Euclid's algorithm for greatest common divisor欧几里德最大公约数算法

              6 PRINT "Type two integers greater than 0"打印两个大于0的整数

              10 INPUT A,B

              20 IF B=0 THEN GOTO 80

              30 IF A > B THEN GOTO 60

              40 LET B=B-A

              50 GOTO 20

              60 LET A=A-B

              70 GOTO 20

              80 PRINT A

              90 END

            "优雅的"怎样工作: 在"欧几里得循环"的外部位置,"优雅的"在两个"联合循环"之间来来回回转移,A > B 的循环计算A ← A B,B ≤ A的循环计算 B ← B A。这样做是因为,当最后被减数M是小于或等于减数S(差 = 被减数 减数)时、 被减数可以成为s(新的测量长度)和减数可以成为新的r(要测量的长度);换句话说反转减法运算的"感觉"。

    5.3  测试欧几里得算法

            算法做它的作者想要它做的什么吗?几个测试用例通常足以确认核心功能。一个源使用3009和884。克奴思建议40902和24140。另一个有趣的案例是两个相对质数数字14157和5950。

            但特殊的例子必须查明和测试。当R > S、S > R,R = S时"非优雅的"能正确执行吗? 对"优雅的"同上:B > A、A > B,A = B?(全是)。当一个数字是零会发生什么,两个数字都是零呢?("非优雅的"在所有情况下永远计算;"优雅的"当 A = 0时永远计算)。如果输入负数会发生什么呢?分数的数字呢?如果输入的数字即通过该算法/程序计算的函数域要包括仅包括零的正整数,那么在零的失败指示算法(和实例化它的程序)是一个局部函数而不是总的函数。显著的由于例外是失败的原因的是阿丽亚娜V火箭的失败。

            由使用数学归纳法证明程序的正确性:克奴思证明数学归纳法对欧几里得算法的"扩展"版本的应用,他提出"适用于证明任何算法的有效性的一般方法"。陶思沃瑟建议程序复杂性的测量是其正确性长度的证明。

    5.4  测量和改进欧几里得算法

           优雅(简洁度)对良好度(速度): 仅6个核心指令,"优雅"明显的相比"非优雅"的13个指令是赢家。然而,"非优雅"速度更快(它到达HALT(停止)用较少的步骤)。算法分析表明为什么会是这种情况:"优雅"在每个减法循环中进行两个条件测试而"非优雅"只进行一次。由于该算法(通常)需要许多循环穿过,平均在做必须的余数计算后的"B = 0?" 测试上浪费许多时间。

            可以改进算法吗?:一旦程序员判定程序"适合"和"有效"------就是它计算的作者意欲的函数------那么问题变成了,可以改进吗?

            "非优雅"的简洁性可以通过消除5个步骤来改进。但柴厅证明了压缩一个算法不能由一个广义的算法自动压缩的;相反,它仅可以试探性地,即通过穷举搜索(在繁忙的海狸(Busy beaver可以找到例子)、试验和错误、聪明、洞察力、应用归纳推理等。观察在步骤11、12和13中重复了步骤4、5和6。与"优雅"的比较提供着可以消除这些步骤和步骤2、步骤3的暗示。这就减少了核心指令数目从13到8,这使得它的9个步骤比"优雅"的"更优雅"。

            "优雅"的速度可以通过把B = 0?测试移出两个减法循环外改善。这个变化调用添加3个指令(B = 0?,A = 0?,GOTO)。现在"优雅"计算示例数字更快;不管对任何给定的A、B、R、S,这总是需要进行详细的分析的案例。

    6  算法分析

            知道对一种给定的算法理论上需要有多少特定的资源(如时间或存储)常常是重要的。已开发算法分析的方法来获得这种定量的答案(估计数);例如,上面的排序算法有时间条件的o(n),使用大O表示法与n作为列表的长度。在所有的时间算法只需要记住两个值:目前为止发现的最大数和它在输入列表中的当前位置。因此说有一个空间的条件o(1),如果这个空间要求存储的输入数字没有被计数到,或o(n),如果它计数到了。

            不同的算法可能会用或多或少时间、空间或比其它的'努力'的不同的指令集完成相同的任务。例如,二进制搜索算法当用于表查找关于排序列表时通常优于强力顺序搜索。

    6.1  正式对实证

            算法的研究与分析是计算机科学的学科并经常不使用特定的编程语言或执行抽象地实践。在此意义上,算法分析类似于其它数学学科,它集中在算法的基础属性而不是任何特定实现的细节。通常的伪代码用于分析,因为它是最简单和最一般的表示形式。然而,最终,大多数算法通常被实施在特定的硬件/软件平台上和其算法的效率最终被使用实际的代码进行测试。对于一个"一次性"问题的解决方案,特定算法的效率可能没有重大的后果(除非n是极大的),但对于设计用于快速交互、商业或长生命科学用途的算法,它可能是至关重要的。频繁地从小n到大n缩放暴露低效的否则是良性的算法。

            实证测试非常有用,因为它可能发现意外的会影响特性的相互作用。基准可用于比较程序优化后一个算法之前之后潜在的改进。

    6.1.1  FFT 加速

            为了说明即使在一些极度"确立好的"算法的可能的潜在改进,最近的重大创新,与FFT算法(非常沉重的用于图像处理的领域)有关的,可能减少了处理时间高达10,000的一个因素。这个加速的影响,例如,使得便携式计算设备(以及其它设备)功耗更小。

    7  分类

            有各种方法进行算法分类,每个都有其本身的益处。

    7.1  执行

    • 递归或迭代:递归算法是一种反复调用(使参考)自身直到匹配特定条件,这是一种通用的函数式编程的方法。迭代算法使用像循环一样的重复构造和有时增加如堆栈的数据结构来解决给定的问题。有些问题自然适合于一个执行或另一个执行。例如,河内塔(towers of Hanoi是很好理解的递归执行。每个递归版本有一个等效(但可能或多或少复杂的)的迭代版本,反之亦然。
    • 逻辑: 一种算法可能会被看作是受控的逻辑推演。这一概念可表示为:算法 = 逻辑 + 控制。逻辑组件表示可能会在计算中使用的公理,控制组件确定在其中对公理采用推演的方式。这是逻辑编程范式的基础。在纯逻辑的编程语言中控制组件被固定,算法由唯一逻辑组件提供指定的。这种方法呼吁的是优雅的语义:公理的改变在算法中具有定义完善的更改。
    • 串行或并行或分布式:算法通常讨论成假定计算机一次执行一条算法的指令。这些计算机有时称为串行计算机。为这样一种环境设计的一种算法称为串行算法,而不是并行算法或分布式的算法。并行算法利用计算机体系结构几个处理器在同一时间可以工作在一个问题,而分布式算法利用与网络连接的多台计算机。并行或分布式算法将问题划分为更对称或不对称的子问题,并一起收集回结果。在这种算法中的资源消耗不只是每个处理器上的处理器周期,而且也有悬挂的处理器之间通信的。排序算法可以有效地并行,但它们的悬挂通信是昂贵的。迭代算法一般并行的。有些问题没有并行算法,称为本质上串行问题。
    • 确定性或非确定性:确定性算法在算法的每一步用确切决定解决问题,而非确定性算法通过猜测解决问题尽管典型猜测使用试探法作出更准确的。
    • 精确或近似:虽然许多算法达成确切的解决办法,近似算法寻求逼近真正的解决方案。逼近可能使用确定性或随机策略。这种算法对许多困难问题具有实用价值的。
    • 量子算法运行在量子计算的现实模型上。这个术语通常用于那些好像本身是量子的算法,或用于如量子超位或量子纠缠的量子计算的一些基本特征。

    7.2  设计范式

            算法分类的另一个方法是按其设计方法或范式。有一定数量的范例,彼此各有不同。此外,每个类别包括许多不同类型的算法。一些常见的范式包括:

    • 强力的或耗尽的搜索。这是天真的尝试每一个可能的解决办法,确定哪个是最佳方法。
    • 分而治之。分而治之算法一再降低同样的问题(通常以递归方式)到一个或多个较小的实例直到实例都足够小可以容易地解决为止。一个这样的分而治之的例子是合并排序。在把数据划分成段后对每个数据段排序,整个数据排序可以通过合并段的排序获得。一个更简单的分而治之的变形称为减而治之算法,解决完全相同的子问题并使用这个子问题的解决方案来解决更大的问题。分而治之把问题划分成多个子问题,所以征服阶段就比减而治之的算法更复杂。减而治之算法的一个例子是二进制搜索算法。
    • 动态编程。当一个问题显示最优子结构时,意味着解决问题的最佳方法可以从子问题的最优解构造,重叠子问题意味着相同的子问题用来解决许多不同的问题例,更快的方法称为动态编程可以避免已经计算过的重复计算的解决方案。例如,福罗依德-瓦尔沙尔(Floyd–Warshall)算法,从加权图中的一个顶点到一个目标的最短路径可通过使用所有相邻顶点到目标的最短路径找到。动态编程和函数结果备忘录(memoization:turning [the results of] a function into something to be remembered走到一起的。动态编程和分而治之之间的主要区别是在分而治之中子问题更多或更少是独立的,而在动态编程中子问题重叠的。动态编程和直接递归之间的区别是递归调用在缓存或函数结果备忘录中的。当子问题是独立的且没有重复时,函数结果备忘录没有帮助;因此动态编程不是所有复杂问题的解决方案。通过使用函数结果备忘录或维护一个子问题的表已经解决,动态编程降低多项式复数的许多指数性质的问题。
    • 贪婪的方法。贪婪算法类似于一种动态编程算法,但不同的是子问题的解决办法不必在每个阶段都知道;用一个"贪婪"的选择可以做到此时什么看起来最好。贪婪方法基于当前局部最优算法和前一阶段做的最好的决定(并不是所有可能的决定)之上在算法阶段用最佳可能的决定(并不是所有可行的决定)延伸解决方案。它不是耗尽的并且对很多的问题不给一个准确的答案。但当它工作时,它是最快的方法。最受欢迎的贪婪算法就是找到哈夫曼树(Huffman Tree、克拉思卡尔(Kruskal)、普里木(Prim、索林(Sollin)给定的最小生成树。
    • 线性(编程)规划。当使用线性编程解决问题时,发现了输入涉及的特别不等性,然后尝试最大化 (或最小化)输入的一些线性函数。很多问题(如定向图的最大流)可以以线性的编程方式表示,然后由'通用'的算法,如辛普莱(单纯形)算法解决。线性规划问题的一个更复杂变形称为整数编程,其中解决方案空间被限制为整数。
    • 还原。这项技术涉及通过将困难的问题转换为更好地我们知道有(希望如此)渐近最优算法的问题来解决这个困难的问题。目标是要找到一种还原算法其复杂性不被此还原的算法所支配。例如,在涉及第一排序的列表 (昂贵的部分)的未排序的列表中查找中位数,然后在排序的列表(廉价部分)中拉出中间元素的选择算法。这种技术也称为变换和征服。
    • 搜索和枚举。许多问题(例如下棋)可作为图上建模的问题。图探索算法指定围绕图形移动的规则,对于这类问题很有用。此类别还包括搜索算法、 分支和绑定的枚举和回溯。

            随机的算法就是那些做一些随机的(或伪随机)选择;对于某些问题,事实上可以证明最快的解决方案必须有一些随机性。有两大类的这种算法:

            1.    蒙特卡罗算法高概率返回正确的答案。例如RP(随机多项式)是那些运行在多项式时间内的子类。

            2.    拉斯维加斯算法总是返回正确的答案,但其运行的时间只是概率上约束的,例如分散化。

            在优化问题中试探式算法不尝试找到最佳的解决方案,而是找到时间或资源都有限的近似解。它们不实用于找到完美的解决方案。这个的示例是本地搜索、禁忌搜索算法或模拟退火算法,这是一类通过随机量变化问题的解决的试探式的概率算法。"模拟退火"名称暗示加热和冷却金属实现无缺陷的冶金术语的含义。随机变量的目的是找到接近全局最优的解决方案,而不是只是局部最优的,这个想法是随着该算法安定到解决方案随机元素减少。近似算法是那些额外提供一些边界错误的试探式算法。遗传算法尝试通过模仿生物的进化过程找到解决问题的办法,有一个随机突变产生连续几代的"解决方案"的周期。因此,它们仿效繁殖和"适者生存"。在遗传编程中,这种方法扩展到算法,通过把算法本身作为一种问题有关的"解决方案"。

    7.3  研究范畴

            每个科学范畴有它自己的问题,需要高效的算法。一个范畴相关的问题经常在一起研究。一些分类的示例是搜索算法、排序算法、合并算法、数值算法、图论算法、字符串算法、计算几何算法、组合算法、医疗算法、机器学习、加密、数据压缩算法和分析技术。

            范畴往往相互重叠,一个范畴中的算法研究进展可能会改善那些其它的、有时完全不相关的领域。例如,动态规划被发明在优化工业的资源消耗中,但现在用于解决很多领域的广泛范围的问题。

    7通过复杂性

            算法可以按它们完成所需的时间与其输入的大小相比分类。有各种各样的:一些算法以相对输入大小的线性时间完成、一些按时间的指数量这样做或更糟的,一些从未停止。此外,某些问题可能有多个复杂程度不同的算法,而其它问题可能没有算法或没有已知的高效算法。也有从几个问题映射的其它的问题。所以,已经发现,也许基于它们最佳可能的算法的复杂性更适合于把问题自身分类而不是算法的等价划分为类。

            波尔金(Burgin ,2005 年,p.24) 使用放松有限步骤后确定函数计算的算法的输出的共同条件的算法的广义定义。他将一类超级递归算法定义为"一种算法,它可能计算任何图灵机不可计算的函数"( Burgin, 2005年,p.107)。这息息相关于超计算方法的研究。

    8  持续算法

            形容词"持续的"应用于"算法"一词意味着:

            1 一种算法对代表连续量数据进行操作,即使这种数据由离散近似表示------这种算法在数值分析中研究;

            2 一种连续操作数据的微分方程形式的算法,在模拟计算机上运行的。

    9  法律问题

            算法本身通常不是可申请专利的。在美国,单一的抽象概念、数字或信号的简单操作组成的权利并不能构成"过程"(USPTO 2006年),因此算法不是可以申请专利的 (如在高沙尔克诉本森(Gottschalk v. Benson中)。然而,算法的实际应用有时可申请专利。例如,在戴梦德诉叠尔(Diamond v. Diehr中,简单的帮助合成橡胶固化的反馈算法的应用被认为是专利。软件的专利是极具争议性的,有涉及算法被高度批评的专利,特别是数据压缩算法,如Unisys的LZW专利。

            此外,一些加密算法有出口限制 (见加密的出口)。

    10  词源

            "算法(Algorithm)"或在某些其它书面版本中的"算法(Algorism "一词,来自名称al-Khwārizmī,在古典阿拉伯语发音为Al-Khwarithmi。Al-Khwārizmī (波斯语:الخوارزمي,c.780-850)是一位波斯的数学家、天文学家、地理学家和巴格达皇宫的智慧学者,其名字的意思是" Khwarezm的本地人",Khwarezm在他的时代期间是更伟大的伊朗的一部分的一个城市,现在是在现代的乌兹别克斯坦。约825年,他写了一篇阿拉伯语言的论文,在12世纪被翻译成拉丁文,标题是Algoritmi de numero Indorum。这个标题意思是"关于印度人的数字的Algoritmi","Algoritmi"是翻译者的Al-Khwarizmi'的名字的拉丁化。在中世纪末期阿尔-克沃理滋米是在欧洲最为广泛阅读的数学家,主要是通过他的其他书,代数(Algebra。在中世纪后期的拉丁文algorismus,他的名字的变体,只意味着"十进制数字系统",仍然是现代英语算法的含义。在17世纪法国这一词的形式,但不是它的意思,更改为algorithme。不久之后英语采纳了法语的,但直到19世纪后期的" Algorithm(算法)"有了它在现代英语中的意义。

            替代的词源声称词源于中世纪晚期" Arabic arithmetics(阿拉伯算法)"的意思和希腊的代数术语 arithmos(因而字面意思是"阿拉伯数字"阿拉伯计算")。Al Kharizmi著作中的Algorithms不是其现代意义上的算法,而是作为一种类型的重复性算法(在这里要提到他的称为代数的基本著作最初标题为"通过完成和平衡的计算的简明的书"描述重复计算的类型及二次方程)。在这个意义上,算法在欧洲在阿尔·克沃理滋米之前很久已知。今天已知的最古老的算法是欧几里得算法 (也参见扩展欧几里德算法)。在造出算法术语之前,希腊人把它们称为 anthyphairesis,字面的意思是反减法或互减法。希腊人在欧几里德前的几个世纪知道算法。希腊人使用术语arithmetica(ἀριθμητική,即在迪奥范特司(Diophantus的著作中所谓的"代数之父"-请参阅维基百科的文章Diophantine equation和Eudoxos)代替algebra

    11  历史: "算法"概念的发展

    11.1  起源

            算法一词来自9世纪波斯数学家阿布阿卜杜拉·穆罕默德·本·穆萨·阿尔-克沃理滋米(Abu Abdullah Muhammad ibn Musa Al-Khwarizmi,其著作基于第七世纪印度数学家婆罗古普塔(Brahmagupta。算法在最初指使用印度阿拉伯数字执行运算的规则,但通过欧洲拉丁文翻译阿尔-克沃理滋米的名字在18世纪演变成算法。词的使用演变成包括所有明确解决问题或执行任务的程序。

    11.2  离散和可区分符号

            计数标记:为跟踪的他们的羊群、他们的一袋袋粮食和他们的钱古人用符计数:积累石子或标记刻在长棍上或在粘土上制作离散符号。通过巴比伦和埃及的标记和符号的使用,最终演变出了罗马数字和算盘(Dilson,p.16–41)。符标记突出地出现在图灵机和后图灵机计算中使用的一元数字系统算术中。

    11.3  操作符号作为"位置把持者":代数

            古希腊几何学家(欧几里德算法)、印度数学家婆罗古普塔和波斯数学家阿尔-克沃理滋米(" algorism "和" algorithm "术语从其名称派生的)和西方的欧洲数学家的工作最终的导致莱布尼茨(Leibniz的微积分致比率子(calculus ratiocinator1680)的概念:

            他的时代以前的一个半世纪是好年景的,莱布尼兹提出一种逻辑代数,一种以普通代数的指定操作数字规则的方式指定操作逻辑概念的规则的代数。

    11.4  机械构造与离散状态:

            时钟:波尔特把重量驱动的时钟的发明誉为"[中世纪欧洲的]的关键发明",特别是机械时钟给我们提供滴答和刻度线的边缘的擒纵机构。"精确的自动机"立即导致了13世纪开始的"机械自动机",最后到"计算的机器"------19世纪中期的查尔斯·巴贝奇(Charles Babbage)和艾达·洛夫莱斯伯爵夫人(Countess Ada Lovelace)的差分引擎和分析引擎。洛夫莱斯被誉为第一个创造用于计算机处理算法的人------巴贝奇的分析引擎,第一个被视为真正的图灵完备的计算机的装置而不只是一个计算器------结果是有时被称为"史上第一个程序员",虽然巴贝奇的第二个装置的充分执行直到她死后几十年都没实现。

            逻辑机器1870年------斯坦雷·杰文斯(Stanley Jevons的"逻辑算盘"和"逻辑机":技术上的问题是当表现在类似于现在所谓的卡诺图的形式中时减少布尔方程。杰文斯(1880) 首先描述了一个简单的"算盘"的"具有专门提供的销钉的木滑动块,可以有目的的把[逻辑]组合的任何部分或任何类别机械地挑选出来...... 不过最近我把系统减少成完全机械的形式,因而体现了间接推断整个过程中什么可称为是一个逻辑机器的"。他的机器配有"某些可移动的木杆"和"脚下是21个键像那些钢琴 [等]一样的......."。用这种机器他可以分析一个"推论式或任何其他简单的逻辑论据"。

            他在1870年在英国皇家学会院士之前显示了这台机器。但是,另一个逻辑学家约翰·维恩(John Venn在他1881年的符号逻辑中,对这一努力投以有色的眼光:"我自己对有时被称为的逻辑机器没有高估计的兴趣或重要性的......它似乎并不给我任何一丝一毫目前已知的或可能被人发现真正值得的的逻辑机器名称";更多阅览算法的特征。但不甘示弱他也提出"有些类似的计划,我理解,对杰文斯教授的算盘......再一次,对应于杰文斯教授的逻辑机,下列发明可能被描述。我更喜欢叫它仅仅是一个逻辑图的机器......但我想它能非常完整做所有可以合理地预期的任何逻辑机器的"。

            提花织机、霍勒里斯打孔卡、电报和电话------机电继电器:贝尔和聂威尔(BELL,Newell ,1971年)表明提花织机(1801年)、霍勒里斯卡(打孔卡,1887年)的前体和"电话交换技术"是导致发展第一个计算机的树的根部。到19世纪中期的电报,电话的前体在整个世界通用,它的离散和可区分的"点线和虚线"的字母编码,像一个共同的声音。19世纪后期使用自动收报机纸条(19世纪70年代),随着是霍勒里内斯卡在1890年美国人口中的使用。然后电传机 (ca.1910年) 到来,有使用带上的波德(Baudot)代码的打孔纸。

            电话交换网络的机电继电器(1835年发明)是在乔治·斯提比茨(George Stibitz1937)工作后的,他是数字加法设备的发明者。在他在贝尔实验室工作时,他观察到"使用带齿轮机械计算器的沉重负担"。1937年的一天晚上他回家打算测试他的想法......当修补的工作结束时,斯提比茨已经构建了一个二进制加法装置"。

            戴维斯(Davis ,2000 年)注意到机电继电器的特别重要性(有两个"二进制状态"的开和闭):

            只是1930年代开始的使用电气继电器发展的机电计算器,机器才被建造成了巴贝奇曾预见的"。

    11.5  19世纪到20世纪中期的数学

            符号和规则:在快速连续的乔治·布尔(George Boole,1847年、1854年)、高特劳波·弗雷格(Gottlob Frege ,1879 年)和朱塞佩·皮诺(Giuseppe Peano,1888–1889)的数学中,把算术还原成规则所操纵的符号序列。皮诺的"新方法表现的算术原则(1888年) 是"以符号语言形式的数学公理化的第一次尝试"。

            但黑杰诺(Heijenoort)给予弗雷格(1879年)这个荣誉:弗雷格的或许是"以往写的逻辑的最重要的单一著作。在其中我们看到一种..."'公式语言',这是一种符号语言,用特殊符号写的语言,"为纯粹的思维",就是免于修辞的点缀...按照一定的规则操作的特定符号构造的一种语言"。弗雷格的工作进一步被阿尔弗雷德·诺斯·怀特海德(Alfred North Whitehead)和伯特兰·罗素(Bertrand Russell)在他们的“数学原理”(1910–1913年)中简化和扩展。

            悖论:在同一时间在文学中出现了一些令人不安的悖论,尤其是布拉力-佛提(Burali Forti)悖论(1897年)、罗素(Russell)悖论(1902–03年)和理查德(Richard)悖论。结果的考虑导致库尔特·高德尔(Kurt Gödel的论文(1931年)------他特别引述谎言者的悖论------这完全把递归规则还原成数字。

            有效计算:在努力解决1928年由希尔伯特定义的决策问题(Entscheidungs problem),数学家们第一次开始定义"有效方法"或"有效计算"或"有效计算性"(即能成功的计算)所指的是什么。在快速连续中以下的出现:阿隆索·丘尔奇(Alonzo Church)、斯蒂芬·克林(Stephen Kleene)和罗瑟(J.B. Rosser)的λ演算,高德尔对加克·赫布兰德(Jacques Herbrand参看高德尔1934年普林斯顿大学演讲)的建议的行动的著作中精心磨砺定义的"一般递归"并随后被克林简化。丘尔奇的证明决定问题(Entscheidungs problem)是不能解决的,爱米尔·泊思特(Emil Post)的定义有效计算为一个工人盲目遵照指令表的说明通过一系列的房间向左或向右移动同时在这些地方要么标记或擦除一张纸或观察纸和做出下一条指令的是否决定。阿兰·图灵的证明决策问题( Entscheidungsproblem)使用他的效果几乎与泊思特的"公式"相同的"-[自动] 机"是不能解决的,巴克利·罗瑟的按照"一台机器"定义的"有效方法"。克林提议的他称为的"论文I"的"丘尔奇论文"的前提,几年后克林重命名他的论文"丘尔奇的论文"并提出"图灵的论文"。

     11.6  米尔·泊思特(Emil Post,1936) 和阿兰·图灵 (1936–37,1939年)

            两人彼此不知道对方这是一个惊人的巧合,但描述人作为计算机对计算工作的过程------他们产生本质上相同的定义。

            米尔·泊思特(1936年)如下所述的"计算者"(人类)的动做:

            "......涉及两个概念:一个符号空间其中要进行从问题到回答的工作,和一组固定不可改变的方向集。

            他的符号空间是"两种方式的无限序列的空间或框......问题解决者或工作者在此符号空间移动,并能够在中和操作,但一次只在一个框..... 一个框只承认两个可能的条件,即,空的或未标记的和有一个单一的标记就说垂直描边"。一个框被挑出来并称为起始点。...一个具体问题用有限数量的标有描边的框[即输入]以符号的形式给定。同样的答案[即输出]是这样配置的标记框以符号形式给定的....

            一套适用于一个普遍问题的方向当应用到每个具体的问题时设置一个确定的过程。这一过程只有当它到类型(C)[即,停止] 的方向时终止"。更多见Post–Turing机。

    在布莱奇利公园的阿兰·图灵的雕像。

            阿兰·图灵的工作先于斯提比茨(Stibitz 1937)前;不知道是否斯提比茨知道图灵的工作。图灵的传记作者认为图灵使用的打字机样模型是从青春的兴趣派生的:"阿兰小孩时都梦到发明打字机;图灵夫人有一台打字机;他好可能已开始询问自己把打字机称为'机器'是什么意思"。由于摩尔斯电码电报、收报机和电传打字机普遍存在我们可能猜想都受影响。

            图灵------他的计算模型现在被称为图灵机------开始和泊思特做的一样,通过分析人类计算者他划分成一组简单的基本动作与"思维的状态"。但他进了一步,创建了一台数字计算的模型机器。

            "计算通常是在纸张上书写某些符号来做的。我们可以设想这张纸被划分成像一个孩子的算术书的正方块......然后我假设计算是在一维纸上进行的即在分为正方块的带上进行的。我也假设可能会打印的符号数字是有限的......

            "计算者在任何时刻的行为由他正在观察的符号确定,他那一刻的"思维状态"。我们可以假设有B绑定到计算者可以在一个时刻观察的符号数字或正方形。如果他希望看到更多的,他必须使用连续观测。我们还将假定需要考虑的思维状态的数目是有限的......

            "让我们想像由计算者执行的操作会分成'简单操作'是这样的基本难以想象将它们进一步划分。

            图灵的还原产生如下的:

            "简单操作因此必须包括:

            " (a)一个观察到的方块的符号变化

            " (b)一个观察到的方块对另一个以前观察到的方块的L个方块内之一的方块的的变化"。

            可能这些变化的一些一定会引发思维状态的改变。因此最一般的单个操作必须采取下列之一:

            " (A) 一个(a)符号的可能的变化与可能的思维状态的改变"。

            " (B) 一个(b)观察到的方块的可能变化与思维状态的可能的改变"。

            " 我们现在可能构造机器来做这个计算者的工作"。

            几年后,图灵用这个有力的体现扩大了他的分析(论文、定义):

            "说一个函数可以被"有效地计算"是说如果它的值可以被一些纯粹机械的过程找到。虽然很容易直观把握这种想法,但最好还是有一些更明确的、数学上可表达的定义......[他讨论定义的历史尽可能的与上文所述关于高德尔、赫布兰德、克林、丘吉尔、图灵和泊思特的一样表达的]......我们可能会字面上采取此陈述,了解成由一台机器可以执行的纯机械过程。有可能以一定的正规形式给出一个这些机器的结构的数学描述。这些想法的发展导致作者定义可计算的函数,并用有效计算性辩识可计算性标识 †......。" †我们应使用这个表达"可计算函数"来意味一台机器可计算的函数,我们让"有效可计算"指没有这些定义的任何一个特定识别的直观想法"。

    11.7  J.B.罗瑟(Rosser,1939年)和S.C.克林(Kleene,1943)

            巴克利·罗瑟以下列方式(添加粗体)定义'有效的[数学]方法:

            "'这里使用的'有效方法'是相当特殊意义的一种方法,其中的每一步精确地确定和以有限数的步骤一定产生答案。具有这种特殊含义,现在已给定三个不同的精确定义。[他的脚注#5;见立即下文的讨论]。这些的最简单的陈述基本上说明(由泊思特和图灵)一种有效的解决一定问题集的方法存在的如果人可以建造一台机器,这台机器然后没有人类干预解决任何集的问题除插入问题和(以后)阅读答案之外。所有三个定义是等效的,因此无论使用哪一个都一样。此外,所有三个都等效的事实是任何一个的正确性的非常有力的论据"。(罗瑟1939:225–6)

            罗瑟的脚注#5引用(1)丘吉尔和克林的著作和他们的λ-定义性的定义,特别是丘吉尔在他的基本数量理论不能解决的问题(1936年)中它的用法;(2)赫布兰德和高德尔和他们用的递归过程,特别是高德尔在他的著名论文关于数学原理和相关系统的正式不可决定的命题中的使用(1931年);(3)泊思特(1936年)和图灵(1936–7年)他们的计算机理的模型

            斯蒂芬(Stephen参考克林定义他的现在著名的"论文I"为丘吉尔图-灵论文。但他在以下范围内这样做的(原始的粗体):

            "12。算法理论......在建立一个完整的算法理论中,我们所做的是描述一种程序,对每个独立变量的值的集可执行的和哪些过程必须终止并以这种方式从结果我们可以读取"一个明确的答案,问题的"是"或"不是",谓词值true吗? "(克林 1943:273)

    11.8  1950后历史

            许多努力直接指向进一步细化的定义"算法",活动正在进行因为问题环绕的,尤其是数学基础(特别是Church–Turing论文)和思维的哲学(尤其是围绕人工智能的论据)。有关详细信息,请参阅算法特征

     

    http://en.wikipedia.org/wiki/Algorithm

    Text is available under the Creative Commons Attribution-ShareAlike License

     

    展开全文
  • 算法什么,为什么要对算法进行研究,相对于计算机中使用的其他技术来说,算法作用什么算法什么? 简单的说,算法就是一个计算过程,你通过这个过程,将输入数据转化为输出结果。就像我们吃东西的过程...
  • 算法算法分析

    2018-07-15 20:21:37
    算法与数据结构的关系十分的密切,在设计算法时先确定相应的数据结构,而在讨论某种数据结构时,也必然涉及相应的算法,下面就从和算法的...一个算法应该有下列特性。 ①有穷性。一个算法必须在有穷步之后结束,...
  • 从K近邻算法、距离度量谈到KD树、SIFT+BBF算法

    万次阅读 多人点赞 2012-11-20 16:31:35
    从K近邻算法、距离度量谈到KD树、SIFT+BBF算法前言 前两日,在微博上说:“到今天为止,我至少亏欠了3篇文章待写:1、KD树;2、神经网络;3、编程艺术第28章。你看到,blog内的文章与你于别处所见的任何都不同。于是...
  • 排序算法

    2016-03-30 16:41:33
    定义1. 排序排序(Sorting) 是计算机程序设计中的一种重要操作,它的功能是将一类数据元素的任意序列,重新排列成一个关键字有序的序列。...基本上,排序算法的输出必须遵守下列两个原则: 输出结果为递增序列
  • 算法】聚类算法

    千次阅读 2018-04-24 16:29:41
    1.1 什么是聚类 聚类是数据挖掘中的概念,就是按照某个特定标准(如距离)把一个数据集分割成不同的类或簇,使得同一个簇内的数据对象的相似性尽可能大,同时不在同一个簇中的数据对象的差异性也尽可能地大...
  • 苏联哲学百科:什么是算法?

    千次阅读 2007-02-04 20:56:00
    算 法 (Algorithm)   (译自苏联哲学百科全书)  所谓算法,就是确定...例如,中学的代数就已经有字母的计算,如 (x+y)*(x-y)=x*x-y*y( 虽然这里的字母还只起替代数字的作用 ) ;就在算术的计算中已经出现了不代表任
  • 例题:下列给定程序中,函数fun的功能是:利用插入排序法对字符串中的字符按从小到大的顺序进行排序。插入法的基本算法是:先对字符串中的头两个元素进行排序;然后把第三字符插入到前两个字符中,插入后前三个字符...
  • Adaboost 算法的原理与推导

    万次阅读 多人点赞 2014-11-02 23:31:07
    Adaboost 算法的原理与推导 0 引言 一直想写Adaboost来着,但迟迟未能动笔。其算法思想虽然简单:听取多人意见,最后综合决策,但一般书上对其算法的流程描述实在是过于晦涩。昨日11月1日下午,在我组织的...
  • 什么是 k-means 聚类算法

    千次阅读 2008-01-05 13:47:00
    K-MEANS算法 K-MEANS算法:输入:聚类个数k,以及包含 n个数据对象的数据库。输出:满足方差最小标准的k个聚类。处理流程: (1) 从 n个数据对象任意选择 k 个对象作为初始聚类中心;(2) 循环(3)到(4)直到...
  • 满意答案srxgb2018.10.16采纳率:47%等级:12已帮助:18082人#include #include typedef struct Lnode{ int data; struct Lnode *next; }Lnode,*Linklist; Linklist L; Linklist ListInit(Linklist Head, int n) { ...
  • 算法】图的最小生成树(Kruskal算法)

    万次阅读 多人点赞 2019-07-15 17:53:36
    前面介绍了图的最小生成树的Prim算法,这个算法是从顶点的角度来刻画生成树的。今天要说的Kruskal(克鲁斯卡尔)算法,是从边的角度来进行刻画的。 Kruskal算法用到的数据结构有: 1、边顶点与权值存储结构(即图是...
  • 例题:下列给定程序中,函数fun的功能是:用递归算法计算斐波拉契级数数列中第n项的值。从第1项起,斐波拉契级数序列为1、1、2、3、5、8、… 例如,若给n输入7,则该项的斐波拉契级数值为13。 请勿改动主函数main与...
  • ZUC算法

    千次阅读 多人点赞 2020-11-10 16:42:31
    ZUC算法结构2.1 LFSR2.1.1 初始化模式2.1.2 工作模式2.2 BR2.3 非线性F函数3 ZUC算法流程3.1 密钥装入3.2 算法运行3.2.1 初始化阶段3.2.2 工作模式4. ZUC算法安全性总结 前言 ZUC算法是国密算法的一种,它是一种...
  • 优化算法之遗传算法

    千次阅读 2017-05-24 17:36:36
    遗传算法(Genetic Algorithm)是模拟达尔文生物进化论的自然选择和遗传学机理的生物进化过程的计算模型,是一种通过模拟自然进化过程搜索最优解的方法,它最初由美国Michigan大学J.Holland教授于1975年首先提出来的...
  • Kmeans聚类算法详解

    万次阅读 多人点赞 2018-05-16 18:41:40
    摘要:本文通过图文详细介绍Kmeans聚类算法的原理和程序实现,以及如何选取类簇中心点。本文首先介绍利用该算法的原理及理解,详细介绍基于MATLAB设计一个自定义的Kmeans函数过程,然后利用该函数对UCI的数据集进行...
  • 算法初体验之欧几里得算法

    千次阅读 2018-03-07 20:20:30
    本文重点讲述欧几里得算法,引出算法的三大前提,大概阐明算法的一些特点。欧几里得算法(或辗转相除法)用于计算两个正整数的最大公约数,基本算法如下:E:设两个正整数m,n,且已知m&gt;nE1:令r=m%n('%'代表...
  • 遗传算法

    千次阅读 2005-12-19 11:46:00
    遗传算法是根据生物进化思想而启发得出的一种全局优化算法。遗传算法的概念最早是由Bagley J.D在1967年提出的;而开始遗传算法的理论和方法的系统性研究的是1975年,这一开创性工作是由Michigan大学的J.H.Holland所...
  • 路由算法总结

    万次阅读 2016-09-02 14:50:54
    概述通信子网络源节点和目的...确定路由选择的策略称路由算法。设计路由算法时要考虑诸多技术要素。首先是路由算法所基于的性能指标,一种是选择最短路由,一种是选择最优路由;其次要考虑通信子网是采用虚电路还是数
  • 1.3扫描线种子填充算法 1.1和1.2节介绍的两种种子填充算法的优点是非常简单,缺点是使用了递归算法,这不但需要大量栈空间来存储相邻的点,而且效率不高。为了减少算法中的递归调用,节省栈空间的使用,人们提出了...
  • RSA算法原理——(1)目前常见加密算法简介

    万次阅读 多人点赞 2018-06-18 21:50:53
    今天为大家带来RSA算法的讲解文章,主要包括RSA算法的加解密过程和公式论证。文章可能会稍微有点长,但是内容绝对是目前全网最详细的,最通俗易懂的,跟着昌昌来一起揭开RSA非对称加密算法的面纱,保你看完本篇文章...
  • CART算法

    千次阅读 2014-06-26 11:26:30
    CART算法    CART全称为Classification And Regression Tree,分类和回归即是该算法的核心。 分类算法在于生成决策树,CART为递归算法,总是将当前样本(not pure)分为两个子样本集,使得生成的每个非叶子节点...
  • BP算法改进

    万次阅读 2016-08-30 22:27:52
    BP算法的问题 权值初始化 用于权值 初始化 的一个普遍方法是设置为:区间[−0.5N,0.5N]\left [ ...对于单隐层的情况,Nguyen和Widrow认为下列算法能显著提高网络的训练速度。 算法1 计算缩放因子:γ=0.7n1−−√n
  • 导数对机器学习算法作用 1.基本概念 2.引题 3.推导过程 1. 引题 导数本质: 导数的本质是通过极限的概念对函数进行局部的线性逼近,导数实质上就是一个求极限的过程。以下为推导公式: 求导作用: 可以得到一些...
  • 查找算法

    万次阅读 2020-08-23 12:57:53
    它或者是一颗空树,或者是具有下列性质的二叉树。 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值。 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值。 它的左、右子树也分别为二叉...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 80,366
精华内容 32,146
关键字:

下列算法的功能是什么