【算法】1 由插入排序看如何分析和设计算法

插入排序及其解决思路

算法的作用自然不用多说,无论是在校学生,还是已经工作多年,只要想在计算机这条道路走得更远,算法都是必不可少的。

就像编程语言中的“Hello World!”程序一般,学习算法一开始学的便是排序算法。排序问题在日常生活中也是很常见的,说得专业点:

输入是:n个数的一个序列<a1,a2,...,an?1,an>

输出是:这n个数的一个全新的序列<a,1,a,2,...,a,n?1,a,n>,其特征是a,1≤a,2≤...≤a,n?1≤a,n

举个例子,在本科阶段学校往往要求做的实验中大多是“学生管理系统”、“信息管理系统”、“图书管理系统”这些。就比如“学生管理系统”中的分数,每当往里面添加一个新的分数时,便会和其他的进行比较从而得到由高到低或由低到高的排序。

我本人是不太喜欢做这种管理系统的…… 再举个比较有意思的例子。

大家肯定都玩过扑克牌,撇开部分人不说,相信大部分童鞋都热衷于将牌按序号排好。那么这其中就蕴含着算法的思想:

1)手中的扑克牌有2种可能:没有扑克牌(为空)或者有扑克牌且已排好序。
2)从桌上抓起一张牌后,从左往右(从右往左)依次进行比较,最终选择一个合适的位置插入。

简单的说,插入排序的精髓在于“逐个比较”。

在列出代码之前,先来看看下面的第一张图,我画的不是太好,就是有没有经过排序的 “8,7,4,2,3,9”几个数字,根据上面的描述,将排序过程描述为:

1)将第二个数字“7”和“8”比较,发现7更小,
于是将“8”赋值到“7”所在的位置,
然后将7赋值给“8”所在的位置。

2)将”4“移到”7“所在的位置,”7“和”8“后移一位。
3)同样的步骤,将”2“和”3“移到”4“的前面。
4)”9“比前面的数字都大,故不移动。

仅仅是这样的描述还是不够的,我们需要更加专业一点。

1)设置一个循环,从第二个数字开始(索引为1)不断与前面的数字相比。
2)每次循环开始时作为比较的数的索引为j,设置temp为其值。(因为在前面也看到了,将”8“赋值到”7“的位置时,如果不将”7“保存起来,那么便丢失了这个数字。)
3)取得j的前一位i。
4)只要i仍在数组中,并且索引为i处的值大于temp,就将i后一位的值设为i处的值,同时将i减1。
5)在i不在数组中或i处的值不必temp大时结束第四部的循环,然后将i后一位的值设置为temp。

将上面这部分描述翻译为insertion_sort函数,下面是完整的测试程序。

#include <iostream>
#include <cstdio>

using namespace std;

#define MAX_N 1000

int A[MAX_N];
int n;

void insertion_sort();

int main()
{
    printf("数组长度:\n");
    scanf("%d",&n);
    printf("数组内容:\n");
    for(int i=0;i<n;i++)
    {
        scanf("%d",&A[i]);
    }
    insertion_sort();
        for(int i=0;i<n;i++)
    {
        printf("%d ",A[i]);
    }
    return 0;
}

void insertion_sort()
{
    for(int j=1;j<n;j++)
    {
        int temp=A[j];
        int i=j-1;
        while(i>=0&&A[i]>temp)
        {
            A[i+1]=A[i];
            i=i-1;
        }
        A[i+1]=temp;
    }
}

下面是能够帮助我们理解算法的正确性的循环不变式的三条性质:

初始化:循环第一次迭代之前,它为真。
保持:如果循环的某次迭代之前它为真,那么下次迭代之前它仍为真。
终止:在循环终止时,不变式能够提供一个有助于证明算法正确性的性质。

就比如上面排序的例子,终止意味着在最后一次迭代时,由传入数组元素构成的子数组元素都已排好序,因此此时子数组就等同与原数组,于是循环终止。

学习如何分析算法

继续分析排序算法,我们知道排序10000个数肯定要比排序10个数所花费的时间更长,但除了输入的项数外就没有其他的影响因素吗?当然有,比如说输入的序列的已被排序的程度,如果是“23456781”这个序列,我们仅仅需要将1放到首位就好,而输入是”87654321“,我们就需要将7到1依次与其前面的数字进行比较。

关于算法的分析也有两个定义:

1)输入规模,当考虑的是排序算法时,那么规模就指的是项数;如果考虑的是图算法,那么规模就是顶点数和边数。
2)运行时间,名义上来说就是算法执行的时间,但实际上我们在分析一个算法时考量的算法执行的操作数或步数。

下面我们通过前面排序算法的伪代码来分析它的运行时间。

INSERTION-SORT(A)
1   for j = 2 to A.length                  // 代价c1,次数n
2       temp=A[j];                         // 代价c2,次数n-1
3       // 将A[j]插入到已排序的A[1..j-1]      // 代价0,次数n-1
4       i=j-1;                             // 代价c4,次数n-1
5       while i>0 and A[i]>temp            // 代价c5
6           A[i+1]=A[i];                   // 代价c6
7           i=i-1;                         // 代价c7
8       A[i+1]=temp;                       // 代价c8,次数n-1

代价为c1处的次数为n应该比较好理解对吧,从j=1到j=n一共有n步,j=n也应该包括在内,因为这是算法终止的情况。而j=n时,程序直接终止了,所以在代价c2、c3、c7处次数都为n-1。

那么在while循环中呢,代价为c4的时候次数为多少呢,很显然应该是∑nj=2tj,而c5和c6在while循环里总有一次它不会去执行,因此次数为∑nj=2(tj?1)。

将代价和次数相乘,便得到了该算法所需的总时间:

T(n)=c1n+c2(n?1)+c4(n?1)+c5∑nj=2tj+c6∑nj=2(tj?1)+c7∑nj=2(tj?1)+c8(n?1)

除此之外我们还可以来对算法进行最好和最坏情况的分析:

1)在最好情况下,也就是整个输入数组其实都是排好序的,那么它根本没法进入while循环,也就是说当i取初值j-1时,有A[i]≤temp,从而对j=2,3,4...n都有tj=1。

那么算法的总时间也就可以算出来了:

T(n)=(c1+c2+c4+c5+c8)n?(c2+c4+c5+c8)

2)在最坏情况下,也就是数组是逆向排好序的,那么就需要将A[j]与已排好序的数组A[1...j1]中的每个元素进行比较,从而对j=2,3,4...n都有tj=j。

那么算法的总时间也就可以算出来了:

T(n)=(c52+c62+c72)n2+(c1+c2+c4+c52?c62?c72+c8)n?(c2+c4+c5+c8)

渐近记号

Θ

在上面我已经求出了该排序算法的运行总时间,但我们可以对其做进一步的简化以及抽象,我们只考虑最坏情况,因为在实际中它更有意义。将运行时间表示为an2+bn+c,其中的a、b、c都依赖于ci。

让我们来做进一步的抽象,通过只考虑运行时间的增长率和增长量级。因为当程序足够大时,低阶项便不再重要了,甚至连高阶项的系数也可以忽略不计。于是,我们记插入排序在最坏情况下所需要的运行时间为Θ(n2)。

现在是时候给出Θ的定义了:

Θ(g(n))={f(n):存在正数常量c1、c2和n0,使得对所有的n≥n0,有0≤c1g(n)≤f(n)≤c2g(n)}

也就是说在跨过n0之后,f(n)就一直处于c1g(n)和c2g(n)之间,其中c1g(n)是下界,c2g(n)是上界。

O和Ω

O和Θ相比,前者就只是后者的一半——只有渐近上界,而没有渐近下界。那么它的定义为:

O(g(n))={f(n):存在正数常量c和n0,使得对所有的n≥n0,有0≤f(n)≤cg(n)}

Ω和Θ相比,前者就只是后者的一半——只有渐近下界,而没有渐近下界。那么它的定义为:

Ω(g(n))={f(n):存在正数常量c和n0,使得对所有的n≥n0,有0≤f(n)cg(n)≤cg(n)}

设计分治算法

前面的排序问题使用的方法叫做增量法,即在排序子数组A[1...j?1]后,将单个元素A[j]插入到子数组的适当位置,然后参数排序好的数组A[1...j],它的精髓在于“逐个比较”。

现在我们再来介绍一种新的方法,叫做分治法,它同样也是鼎鼎大名。它的精髓在于“一分为二“,而驱动这个算法的这是递归。

分治算法在每层递归中都有三个步骤:

1)分解原问题为若干子问题,这些子问题是缩小版的原问题。(抽象的讲,将一个已经切成楔形的大块西瓜可以再切成多个小的楔形西瓜。)
2)解决这些子问题,递归地求解各个子问题。然后,若问题的规模足够小,则直接求解。(继续举例子,要吃完一大块西瓜,可以不断的吃小部分,当西瓜块足够小时,可以一口干掉。)
3)合并这些子问题的解成原问题的解。(吃完的那些小块西瓜加在一起就是刚才那一块很大的西瓜了。)

虽然西瓜的例子能够体现分治算法的思想,但用前面的扑克牌来演示则更加合适,毕竟它有数字呀。来来来,想象一下,桌上正有两堆牌,且分别都已经排号顺序,可是呢我们需要这两堆牌合并起来并且排序好。

那么怎么操作呢?很简单,一句话就能说清楚:不断从两堆牌的顶上选取较小的一张,然后放到新的扑克牌堆中。

首先我们将扑克牌定义成数组A,p和q以及r都是数组的下标,且p≤q<r,两段已排序好的子数组是A[p..q]和A[q+1..r],我们需要做的是将其排序为A[p..r]。下面的伪代码便实现了这个思想:

MERGE(A,p,q,r)
1  n1 = (q - p) + 1 = q - p + 1
2  n2 = (r - (q + 1)) +1 = r - q
3  let L[1..n1+1] and R[1..n2+1] be new arrays
4  for i = 1 to n1
5      L[i] = A[p + i -1]
6  for j = 1 to n2
7      R[j] = A[q + j]
8  L[n1 + 1] = #
9  R[n2 + 1] = #
10 i = 1;
11 j = 1;
12 for k = p to r
13     if L[i] 小于等于 R[j]
14        A[k] = L[i];
15        i = i + 1;
16     else
17        A[k] = R[j]
18        j = j + 1;

上面的”# “号就是传说中的哨兵牌,每当显露一张哨兵牌时,它不可能为较小的值,除非两个堆都已显露出哨兵牌。但是出现这种情况就意味着算法结束,所有非哨兵牌都已被放置到输出堆。

………………p     q       r………………
………………3 6 8 9 2 5 7 8………………
        k

L 3 6 8 9 #     R 2 5 7 8 #
  i               j

比较”3“和”2“,发现2更小,于是将2放到A数组的首位,并且将j移后一位。

………………p     q       r………………
………………2 6 8 9 2 5 7 8………………
          k

L 3 6 8 9 #     R 2 5 7 8 #
  i                 j

比较”3“和”5“,发现3更小,于是将3放到数组A的首位,并且将i移后一位。

………………p     q       r………………
………………2 3 8 9 2 5 7 8………………
            k

L 3 6 8 9 #      R 2 5 7 8 #
    i                j

以此类推,最终A数组就成排好了序……

………………p     q       r………………
………………2 3 5 6 7 8 8 9………………
                      k

L 3 6 8 9 #      R 2 5 7 8 #
          i                j

将上面的思想以及伪代码写成如下程序,大家可以参考参考:

#include <iostream>
#include <cstdio>

using namespace std;

#define MAX_N 1000

int A[MAX_N];
int L[MAX_N/2];
int R[MAX_N/2];
int n,p,q,r;

void merge();

int main()
{
    printf("数组长度:\n");
    scanf("%d",&n);
    printf("数组内容:\n");
    for(int i=0;i<n;i++)
    {
        scanf("%d",&A[i]);
    }
    printf("输入p q r\n");    scanf("%d %d %d",&p,&q,&r);
    merge();

    for(int i=0;i<n;i++)
    {
        printf("%d ",A[i]);
    }
    return 0;
}

void merge()
{
    int n1=q-p+1;
    int n2=r-q;
    for(int i=0;i<n1;i++)
        L[i]=A[p+i];
    for(int j=0;j<n2;j++)
        R[j]=A[q+j+1];
    L[n1]=100;
    R[n2]=100;
    for(int k=p,i=0,j=0;k<=r;k++)
    {
        if(L[i]<=R[j])
        {
            A[k]=L[i];
            i=i+1;
        }
        else
        {
            A[k]=R[j];
            j=j+1;
        }
    }
}

下面没有用图示而是用了代码显示块来显示,应该也能看的吧?就是不断的归并,最终合成一个完整的排好序的数组。

…………………………2 2 5 6 7 8 8 9…………………………
……………………………………归并……………………………………
…………………2 5 6 8………………2 7 8 9…………………
……………………归并…………………………归并……………………
……………2 6…………5 8…………2 9…………7 8……………
……………归并…………归并………归并……………归并……………
…………6……2………8……5………2……9………8……7…………

要完成上面这个程序,我们可以利用前面写好的merge函数呢,下面是伪代码实现。

MERGE-SORT(A,p,r)
1  if p < r
2      q = 小于或等于(p+r)/2的最大整数
3      MERGE-SORT(A,p,q)
4      MERGE-SORT(A,q+1,r)
5      MERGE(A,p,q,r)

然后为了完成这其中q的参数传递,将它们设置为全局变量已经不合适了,具体的程序可以参考如下:

#include <iostream>
#include <cstdio>

using namespace std;

#define MAX_N 1000

int A[MAX_N];
int L[MAX_N/2];
int R[MAX_N/2];

void merge(int A[],int p,int q,int r);
void merge_sort(int A[],int p,int r);

int main()
{
    int n, p, q, r;
    printf("数组长度:\n");
    cin >> n;
    printf("数组内容:\n");
    for (int i = 0;i < n;i++)
    {
        cin >> A[i];
    }
    printf("输入p r\n");        cin >> p >> r;

    merge_sort(A, p, r);

    for (int i = 0;i < n;i++)
    {
        printf("%d ", A[i]);
    }

    return 0;
}

void merge_sort(int A[],int p,int r)
{
    if(p<r)
    {
        int q=(p+r)/2;
        merge_sort(A,p,q);
        merge_sort(A,q+1,r);
        merge(A,p,q,r);
    }
}

void merge(int A[], int p, int q, int r)
{
    int n1 = q - p + 1;
    int n2 = r - q;
    for (int i = 0;i<n1;i++)
        L[i] = A[p + i];
    for (int j = 0;j<n2;j++)
        R[j] = A[q + j + 1];
    L[n1] = 100;
    R[n2] = 100;
    for (int k = p, i = 0, j = 0;k<=r;k++)
    {
        if (L[i] <= R[j])
        {
            A[k] = L[i];
            i = i + 1;
        }
        else
        {
            A[k] = R[j];
            j = j + 1;
        }
    }
}

分析分治算法

当我们的输入足够小时,比如对于某个常量c,n≤c,则直接求解需要常量时间,并写作Θ(1)。

对于复杂的问题,我们将其分解成a个子问题,每个子问题的规模是原问题的1/b.对于这规模为n/b的子问题,累计需要T(n/b)的时间,所以需要aT(n/b)的时间来求解这a个子问题。而这其中分解的过程也需要消耗一定的时间,记作D(n),合并这些子问题也需要一定的时间,记作C(n)。于是又得到了一个递归式:

当n≤c时,T(n)=Θ(1)

其他情况时,T(n)=aT(n/b)+D(n)+C(n)

下面来通过分治模式在每层递归时都有的三个步骤来分析归并排序算法,我们所考虑的是n个数的最坏情况下的运行时间。同样的,归并排序一个元素需要常量时间,当n>1时有如下3个步骤:

分解:分解步骤仅仅计算子数组的中间位置,需要常量时间,因此D(n)=Θ(1)

解决:我们递归地求解两个规模均为n/2的子问题,将贡献2T(n/2)的运行时间。

合并:一个具有n个元素的子数组上过程MERGE需要Θ(n)的时间,所以C(n)=Θ(n)。

因此D(n)+C(n)便等于Θ(n)和Θ(n)相加,可是结果依然为Θ(n)。接着可以得到归并排序的最坏情况运行时间的递归式了。

当n=1时,T(n)=Θ(1)

当n>1时,T(n)=2T(n/2)+Θ(n)

我们对上式稍作变换如下:

当n=1时,T(n)=c

当n>1时,T(n)=2T(n/2)+cn

这个式子可以不断的做递归,最后形成一个递归树的。树的第一层是cn,第二层是cn/2,第三层是cn/4,直到最后一层为c。第二层有2棵子树,第三层有4棵子树,直到最后一程有n棵子树,因此每层的代价总共为cn。而整个树共有lgn+1层,因此总代价为cnlgn+cn。

忽略低阶项和系数项,最终记为Θ(nlgn)。

递归和迭代

这两个概念也许很多童鞋依旧是老虎老鼠傻傻分不清楚,下面通过求解斐波那契数来看看它们俩的关系吧。

斐波那契数的定义:

f0=0

f1=1

fi=fi?1+fi?2(i>1)

递归:

(factorial 6)
(* 6 (factorial 5))
(* 6 (* 5 (factorial 4)))
(* 6 (* 5 (* 4 (factorial 3))))
(* 6 (* 5 (* 4 (* 3 (factorial 2)))))
(* 6 (* 5 (* 4 (* 3 (2 (factorial 1))))))
(* 6 (* 5 (* 4 (* 3 (* 2 1)))))
(* 6 (* 5 (* 4 (* 3 2))))
(* 6 (* 5 (* 4 6)))
(* 6 (* 5 24))
(* 6 120)
720

迭代:

(factorial 6)
(factorial 1 1 6)
(factorial 1 2 6)
(factorial 2 3 6)
(factorial 6 4 6)
(factorial 24 5 6)
(factorial 120 6 6)
(factorial 720 7 6)
720

递归的核心在于:不断地回到起点。

迭代的核心在于:不断地更新参数。

在下面的代码中:

递归的核心是sum的运算,sum不断的累乘,虽然运算的数值不同,但形式和意义一样。

而迭代的核心是product和counter的不断更新。如上表中,product就是factorial的前2个参数不断的累乘更新成第一个参数;而第二个参数则是counter,其不断的加1来更新自己。

product <- counter * product
counter < - counter + 1
#include <iostream>

using namespace std;

int factorialRecursive(int n);
int factorialIteration(int product, int counter, int max_count);

int main()
{
    int n;
    cout<<"Enter an integer:"<<endl;
    cin>>n;
    cout<<factorialRecursive(n)<<endl;
    cout<<factorialIteration(1,1,n)<<endl;

    return 0;
}

int factorialRecursive(int n)
{
    int sum=1;
    if(n==1)
        sum*=1;
    else
        sum=n*factorialRecursive(n-1);
    return sum;
}

int factorialIteration(int product, int counter, int max_count)
{
    int sum=1;
    if(counter>max_count)
        sum*=product;
    else
        factorialIteration((counter*product),(counter+1),max_count);
}

补充问题:

关于上面的factorialIteration函数,今天收到一份邮件,我也通过再次分析学到了很多,这里罗列一下。



第一个问题:

首先来看相对简单的问题,该童鞋在函数内以两种不同方式加上another_sum=2却有着不同的结果。

int factorialIteration(int product, int counter, int max_count)
{
    int sum=1;
    int another_sum=2;
    if(counter>max_count)
    {
        sum*=product;
        another_sum*=product;
    }
    else
        factorialIteration((counter*product),(counter+1),max_count);
}
int factorialIteration(int product, int counter, int max_count)
{
    int sum=1;
    int another_sum=2;
    if(counter>max_count)
    {
        another_sum*=product;
        sum*=product;

    }
    else
        factorialIteration((counter*product),(counter+1),max_count);
}

因为这个函数声明的是int型的返回类型,但没有用return语句,所以C++自动将其运行的最后一行语句作为了返回语句。所以这两个函数类似于:

int factorialIteration(int product, int counter, int max_count)
{
    int sum=1;
    int another_sum=2;
    if(counter>max_count)
    {
        sum*=product;
        return another_sum*=product;
    }
    else
        factorialIteration((counter*product),(counter+1),max_count);
}

int factorialIteration(int product, int counter, int max_count)
{
    int sum=1;
    int another_sum=2;
    if(counter>max_count)
    {
        another_sum*=product;
        return sum*=product;
    }
    else
        factorialIteration((counter*product),(counter+1),max_count);
}

然而我在CodeBlocks中写的代码不用return是可以的,但在Visual Studio中却是会报错的。

有了这个发现,我原来的代码也可以这样来写:

#include <iostream>

using namespace std;

int factorialRecursive(int n);
int factorialIteration(int product, int counter, int max_count);

int main()
{
    int n;
    cout<<"Enter an integer:"<<endl;
    cin>>n;
    cout<<factorialRecursive(n)<<endl;
    cout<<factorialIteration(1,1,n)<<endl;

    return 0;
}

int factorialRecursive(int n)
{
    int sum=1;
    if(n==1)
        sum*=1;
    else
        sum=n*factorialRecursive(n-1);
    // return sum;   // 去掉这里的return语句
}

int factorialIteration(int product, int counter, int max_count)
{
    int sum=1;
    if(counter>max_count)
        return sum*=product;   // 在这里加上return语句
    else
        factorialIteration((counter*product),(counter+1),max_count);
}


现在来看另一个问题:

#include <iostream>
using namespace std;

int test(int n);
int sum;

int main()
{
    cout<<test(1)<<endl;
    return 0;
}
int test(int n)
{
    sum = 1;
    sum += n;
    if (sum < 5)
        test(n+1);
}

如果设sum为全局变量,那么会在test函数中每一次调用sum=1时都将sum重新赋值为1。整个程序最后输出为5。这个应该没有什么悬念吧?

如果设sum给test内的局部变量,则会在每一次执行int sum=1语句时都会创建一个新的sum对象,它的存放地址和之前的sum并不相同。然后整个程序最后输出意外的是4。

#include <iostream>
using namespace std;

int test(int n);

int main()
{
    cout<<test(1)<<endl;
    return 0;
}
int test(int n)
{
    int sum = 1;
    sum += n;
    if (sum < 5)
        return test(n+1);
    // return sum;   此处有这一行代码命名为程序1,没有这行代码命名为程序2
}

程序1的输出是5,程序2的输出是4。具体函数执行过程如下:

第一步,调用test(1):

int sum=1
sum=2
return test(2)

第二步,调用test(2):

int sum=1
sum=3
return test(3)

第三步,调用test(3):

int sum=1
sum=4
return test(4)

第四步,调用test(4):

int sum=1
sum=5

执行到第四步的时候,由于sum以及不比5小了,所以程序1没有进入if语句而是执行下一句return sum,所以输出为1。

而如果是程序2,也就是没有return sum语句,那么程序在执行完第四步后就会返回到第三步,最终调用(return) sum=4,输出4。



第三个问题:

该童鞋还提到了尾递归,这里我就来说说我的理解,如有问题欢迎大家直接评论或邮件给我。

上面代码中的递归函数factorialRecursive应该没问题的吧。

上面的代码我给其命名为迭代。

int factorialIteration(int product, int counter, int max_count)
{
    int sum=1;
    if(counter>max_count)
        sum*=product;
    else
        factorialIteration((counter*product),(counter+1),max_count);
}

通过在main函数中调用如下代码来执行该函数:

cout<<factorialIteration(1,1,n)<<endl;

当然,也可以另外写一个函数如下:

int factorialIter(int n)
{
    return factorialIteration(1,1,n);
}

并通过在main函数中直接调用该函数来做计算:

cout<<factorialIter(n)<<endl;

函数factorialIteration中的max_count我们称其为“循环不变量”,也就是对于整个运算过程而言这个变量是不变的。为了让大家更加印象深刻,将前面出现过的东西再来复制一遍:

(factorial 6)
(factorial 1 1 6)
(factorial 1 2 6)
(factorial 2 3 6)
(factorial 6 4 6)
(factorial 24 5 6)
(factorial 120 6 6)
(factorial 720 7 6)
720

从第二行开始的factorial的第三个参数”6“就是循环不变量。

尾递归:

在计算机科学中,尾调用是一个作为过程最后一步的子例程调用执行。如果尾调用可能在以后的调用链中再调用这同一个子例程,那么这个子例程就被称为是尾递归,它是递归的一个特殊情况。尾递归非常有用,在实现中也容易处理。尾调用可以不通过在调用堆栈中添加新的栈帧而实现。

传统上,尾部调用消除是可选的。然而,在函数式编程语言中,尾调用消除往往由语言标准作为保障,这种保证允许使用递归,在特定情况下的尾递归,来代替循环。在这种情况下,尽管用它作为一种优化是不正确的(尽管它可能是习惯用法)。在尾递归中,当一个函数调用它自身这种特殊情况下,可能调用消除比传统的尾调用更加合适。

迭代:

迭代是一个重复过程,它的目的是接近既定的目标或结果。每次重复的过程也称为”迭代“,作为迭代的结果都将作为下一次迭代的起点。

迭代在计算中是指的计算机程序中的重复的语句块。它可以表示两个专业术语,同义重复,以及描述一种具有可变状态重复的具体形式。然后令人费解的是,它也可以表示通过显式重复结构的任何重复,而不管其可变性。

在第一个意义上,递归是迭代的一个例子,但通常用”递归“来标记,而不作为”迭代“的例子。

在第二个意义上,(更加狭义地)迭代描述了一种编程风格。这与一个有着更有声明性方法的递归有着鲜明的对比。

第三个意义上,使用while或for循环,以及使用map或fold的函数也被视为迭代。

(以上定义部分摘自英文维基百科)

关于递归和尾递归在函数式编程中的应用也可以看这里:【Scheme归纳】3 比较do, let, loop




感谢您的访问,希望对您有所帮助。 欢迎大家关注、收藏以及评论。



为使本文得到斧正和提问,转载请注明出处:

http://blog.csdn.net/nomasp


版权声明:本文为 NoMasp柯于旺 原创文章,如需转载请联系本人。分享智慧之光,保卫知识产权。

时间: 2024-10-07 02:00:41

【算法】1 由插入排序看如何分析和设计算法的相关文章

【算法】1 由插入排序看怎样分析和设计算法

插入排序及其解决思路 算法的作用自然不用多说.不管是在校学生,还是已经工作多年.仅仅要想在计算机这条道路走得更远,算法都是不可缺少的. 就像编程语言中的"Hello World."程序一般.学习算法一開始学的便是排序算法. 排序问题在日常生活中也是非经常见的,说得专业点: 输入是:n个数的一个序列<a1,a2,...,an?1,an> 输出是:这n个数的一个全新的序列<a,1,a,2,...,a,n?1,a,n>,其特征是a,1≤a,2≤...≤a,n?1≤a,

【算法基础】由插入排序来看如何分析和设计算法

插入排序及其解决思路 算法的作用自然不用多说,无论是在校学生,还是已经工作多年,只要想在计算机这条道路走得更远,算法都是必不可少的. 就像编程语言中的"Hello World!"程序一般,学习算法一开始学的便是排序算法.排序问题在日常生活中也是很常见的,说得专业点: 输入是:n个数的一个序列<a1,a2,...,an?1,an> 输出是:这n个数的一个全新的序列<a,1,a,2,...,a,n?1,a,n>,其特征是a,1≤a,2≤...≤a,n?1≤a,n 举

《BI那点儿事》Microsoft 顺序分析和聚类分析算法

原文:<BI那点儿事>Microsoft 顺序分析和聚类分析算法 Microsoft 顺序分析和聚类分析算法是由 Microsoft SQL Server Analysis Services 提供的一种顺序分析算法.您可以使用该算法来研究包含可通过下面的路径或“顺序”链接到的事件的数据.该算法通过对相同的顺序进行分组或分类来查找最常见的顺序.下面是一些顺序示例: 用来说明用户在导航或浏览网站时产生的点击路径的数据. 用来说明客户将商品添加到在线零售商的购物车中的顺序的数据. 该算法在许多方面都

排序算法系列:插入排序算法

概述 直接插入排序(Straight Insertion Sort)的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的.记录数增1的有序表. – <大话数据结构> 版权说明 著作权归作者所有. 商业转载请联系作者获得授权,非商业转载请注明出处. 本文作者:Coding-Naga 发表日期: 2016年3月24日 原文链接:http://blog.csdn.net/lemon_tree12138/article/details/50968422 来源:CSDN 更多内容:分类 &

01 | 复杂度分析(上):如何分析、统计算法的执行效率和资源消耗?

我们都知道,数据结构和算法本身解决的是“快”和“省”的问题,即如何让代码运行得更快,如何让代码更省存储空间.所以,执行效率是算法一个非常重要的考量指标.那如何来衡量你编写的算法代码的执行效率呢?这里就要用到我们今天要讲的内容:时间.空间复杂度分析.其实,只要讲到数据结构与算法,就一定离不开时间.空间复杂度分析. 而且,我个人认为,复杂度分析是整个算法学习的精髓,只要掌握了它,数据结构和算法的内容基本上就掌握了一半.其实,只要讲到数据结构与算法,就一定离不开时间.空间复杂度分析. 复杂度分析实在太

插入排序算法之直接插入排序和希尔排序

插入排序算法 有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序,这个时候就要用到一种新的排序方法--插入排序法,插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的.个数加一的有序数据. 直接插入排序 直接插入排序的排序思路是:每次将一个待排序的元素与已排序的元素进行逐一比较,直到找到合适的位置按大小插入. 例子: 有序列: 开始时,有序序列只有一个元素就是第一个元素(红色),后面的无序序列(绿色).接下来,取无序序列中

openCV中的findHomography函数分析以及RANSAC算法的详解

本文将openCV中的RANSAC代码全部挑选出来,进行分析和讲解,以便大家更好的理解RANSAC算法.代码我都试过,可以直接运行. 在计算机视觉和图像处理等很多领域,都需要用到RANSAC算法.openCV中也有封装好的RANSAC算法,以便于人们使用.关于RANSAC算法的一些应用,可以看我的另一篇博客: 利用SIFT和RANSAC算法(openCV框架)实现物体的检测与定位,并求出变换矩阵(findFundamentalMat和findHomography的比较) 但是前几天师弟在使用op

建模分析之机器学习算法(附python&amp;R代码)

0序 随着移动互联和大数据的拓展越发觉得算法以及模型在设计和开发中的重要性.不管是现在接触比较多的安全产品还是大互联网公司经常提到的人工智能产品(甚至人类2045的的智能拐点时代).都基于算法及建模来处理.     常见的词汇:机器学习.数据建模.关联分析.算法优化等等,而这些种种又都是基于规律的深度开发(也难怪道德经的首篇就提出道可道非常道,名可名非常名的说法),不管是线性还是非线性,总之存在关联关系,而我们最好理解的就是线性关系,简单的用个函数就能解决.比如我们生活中应用的比较的归纳总结,其

微软数据挖掘算法:Microsoft顺序分析和聚类分析算法(8)

前言 本篇文章继续我们的微软挖掘系列算法总结,前几篇文章已经将相关的主要算法做了详细的介绍,我为了展示方便,特地的整理了一个目录提纲篇:大数据时代:深入浅出微软数据挖掘算法总结连载,有兴趣的童鞋可以点击查阅,本篇我们将要总结的算法为:Microsoft顺序分析和聚类分析算法,此算法为上一篇中的关联规则分析算法的一个延伸,为关联规则分析算法所形成的种类进行了更细粒度的挖掘,挖掘出不同种类内部的事例间的顺序原则,进而用以引导用户进行消费. 应用场景介绍 Microsoft顺序分析和聚类分析算法,根据