算法1 由插入排序看怎样分析和设计算法
Posted yangykaifa
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法1 由插入排序看怎样分析和设计算法相关的知识,希望对你有一定的参考价值。
插入排序及其解决思路
算法的作用自然不用多说。不管是在校学生,还是已经工作多年。仅仅要想在计算机这条道路走得更远,算法都是不可缺少的。
就像编程语言中的“Hello World。”程序一般。学习算法一開始学的便是排序算法。
排序问题在日常生活中也是非经常见的,说得专业点:
输入是:n个数的一个序列
输出是:这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函数。以下是完整的測试程序。
// C Plus Plus
// A是数组,j是遍历整个数组的索引,temp是每次取出来作比較的值
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;
}
}
updated at 2016/09/24
// Java
public int[] insertion(int[] nums) {
for (int i = 1; i < nums.length; i++) {
int tmp = nums[i];
int prev = i - 1;
while (prev >= 0 && nums[prev] > tmp) {
nums[prev + 1] = nums[prev];
prev -= 1;
}
nums[prev + 1] = tmp;
}
return nums;
}
以下是能够帮助我们理解算法的正确性的循环不变式的三条性质:
初始化:循环第一次迭代之前,它为真。
保持:假设循环的某次迭代之前它为真。那么下次迭代之前它仍为真。
终止:在循环终止时。不变式能够提供一个有助于证明算法正确性的性质。
就比方上面排序的样例,终止意味着在最后一次迭代时。由传入数组元素构成的子数组元素都已排好序,因此此时子数组就等同与原数组。于是循环终止。
学习怎样分析算法
继续分析排序算法,我们知道排序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的时候次数为多少呢,非常显然应该是
将代价和次数相乘,便得到了该算法所需的总时间:
除此之外我们还能够来对算法进行最好和最坏情况的分析:
1)在最好情况下。也就是整个输入数组事实上都是排好序的。那么它根本没法进入while循环。也就是说当i取初值j-1时。有
那么算法的总时间也就能够算出来了:
2)在最坏情况下。也就是数组是逆向排好序的。那么就须要将
那么算法的总时间也就能够算出来了:
渐近记号
在上面我已经求出了该排序算法的执行总时间。但我们能够对其做进一步的简化以及抽象,我们仅仅考虑最坏情况,由于在实际中它更有意义。将执行时间表示为
让我们来做进一步的抽象。通过仅仅考虑执行时间的增长率和增长量级。由于当程序足够大时,低阶项便不再重要了,甚至连高阶项的系数也能够忽略不计。于是,我们记插入排序在最坏情况下所须要的执行时间为
如今是时候给出
也就是说在跨过
那么它的定义为:
设计分治算法
前面的排序问题使用的方法叫做增量法。即在排序子数组
如今我们再来介绍一种新的方法,叫做分治法,它相同也是鼎鼎大名。
它的精髓在于“一分为二“。而驱动这个算法的这是递归。
分治算法在每层递归中都有三个步骤:
- 1)分解原问题为若干子问题,这些子问题是缩小版的原问题。
(抽象的讲,将一个已经切成楔形的大块西瓜能够再切成多个小的楔形西瓜。)
- 2)解决这些子问题,递归地求解各个子问题。然后,若问题的规模足够小,则直接求解。
(继续举样例,要吃完一大块西瓜,能够不断的吃小部分,当西瓜块足够小时。能够一口干掉。)
- 3)合并这些子问题的解成原问题的解。
(吃完的那些小块西瓜加在一起就是刚才那一块非常大的西瓜了。
)
虽然西瓜的样例能够体现分治算法的思想。但用前面的扑克牌来演示则更加合适。毕竟它有数字呀。
来来来,想象一下,桌上正有两堆牌,且分别都已经排号顺序,但是呢我们须要这两堆牌合并起来而且排序好。
那么怎么操作呢?非常easy,一句话就能说清楚:不断从两堆牌的顶上选取较小的一张,然后放到新的扑克牌堆中。
首先我们将扑克牌定义成数组
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数组的首位。而且将
………………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的首位,而且将
………………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)
然后为了完毕这当中
#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个数的最坏情况下的执行时间。
相同的。归并排序一个元素须要常量时间,当n>1时有例如以下3个步骤:
分解:分解步骤仅仅计算子数组的中间位置,须要常量时间。因此
解决:我们递归地求解两个规模均为
合并:一个具有n个元素的子数组上过程MERGE须要
因此
接着能够得到归并排序的最坏情况执行时间的递归式了。
当
当
我们对上式稍作变换例如以下:
当
当
这个式子能够不断的做递归,最后形成一个递归树的。树的第一层是
忽略低阶项和系数项,终于记为
递归和迭代
这两个概念或许非常多童鞋依然是老虎老鼠傻傻分不清楚,以下通过求解斐波那契数来看看它们俩的关系吧。
斐波那契数的定义:
递归:
(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);
}
updated at 2016/09/24
// Java
public int factorialRecursive(int n) {
int sum = 1;
if (n == 1) sum *= 1;
else sum = n * factorialRecursive(n - 1);
return sum;
}
public int factorialIteration(int n) {
return factorialIterationCore(1, 1, n);
}
public int factorialIterationCore(int product, int counter, int maxCount) {
if (counter > maxCount)
return product;
else
return factorialIterationCore(counter * product, counter + 1, maxCount);
}
补充问题:
关于上面的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“就是循环不变量。
尾递归:
在计算机科学中,尾调用是一个作为过程最后一步的子例程调用执行。假设尾调用可能在以后的调用链中再调用这同一个子例程。那么这个子例程就被称为是尾递归,它是递归的一个特殊情况。
尾递归非常实用。在实现中也easy处理。尾调用能够不通过在调用堆栈中加入新的栈帧而实现。
传统上。尾部调用消除是可选的。
然而,在函数式编程语言中。尾调用消除往往由语言标准作为保障,这样的保证同意使用递归,在特定情况下的尾递归,来取代循环。在这样的情况下。虽然用它作为一种优化是不对的(虽然它可能是习惯使用方法)。
在尾递归中,当一个函数调用它自身这样的特殊情况下。可能调用消除比传统的尾调用更加合适。
迭代:
迭代是一个反复过程,它的目的是接近既定的目标或结果。
每次反复的过程也称为”迭代“,作为迭代的结果都将作为下一次迭代的起点。
迭代在计算中是指的计算机程序中的反复的语句块。
它能够表示两个专业术语。同义反复,以及描写叙述一种具有可变状态反复的详细形式。
然后令人费解的是,它也能够表示通过显式反复结构的不论什么反复,而不管其可变性。
在第一个意义上,递归是迭代的一个样例,但通经常使用”递归“来标记,而不作为”迭代“的样例。
在第二个意义上。(更加狭义地)迭代描写叙述了一种编程风格。这与一个有着更有声明性方法的递归有着鲜明的对照。
第三个意义上,使用while或for循环,以及使用map或fold的函数也被视为迭代。
(以上定义部分摘自英文维基百科)
关于递归和尾递归在函数式编程中的应用也能够看这里:【Scheme归纳】3 比較do, let, loop
感谢您的訪问。希望对您有所帮助。 欢迎大家关注、收藏以及评论。
为使本文得到斧正和提问。转载请注明出处:
http://blog.csdn.net/nomasp
以上是关于算法1 由插入排序看怎样分析和设计算法的主要内容,如果未能解决你的问题,请参考以下文章