第一部分 基础知识
第一章 算法在计算中的应用
算法定义
算法就是把输入转化为输出的计算过程。
如果对所有输入,算法都能输出正确的结果,那么这个算法就是正确的。反之,输出不正确的结果,或者算法本身导致死循环,这个算法就是不正确的。
只要其错误率可控,不正确的算法也可能是有用的(如大素数算法)。
思考题
1-1 运行时间的比较。假设求解问题的算法需要f(n)毫秒,对下表中的每个函数f(n)和时间t,确定可以在时间t内求解的问题的最大规模n。
1秒钟 | 1分钟 | 1小时 | 1天 | 1月 | 1年 | 1世纪 | |
---|---|---|---|---|---|---|---|
lgn | 2^1 000 | 2^60 000 | 2^3 600 000 | 2^86 400 000 | 2^2.592E9 | 2^3.1104E10 | 2^3.1104E12 |
√n | 10^6 | 3.6E9 | 1.296E13 | 7.465E15 | ∞ | ∞ | ∞ |
n | 10^3 | 6*10^4 | 3.6E6 | 8.64E7 | 2.592E9 | 3.1104E10 | 3.1104E12 |
nlgn | 140 | 4 895 | 204 094 | 3.94E6 | ∞ | ∞ | ∞ |
n² | 32 | 244 | 1 897 | 9 295 | 50 911 | 176 363 | 1 763 632 |
n³ | 10 | 39 | 153 | 442 | 1 273 | 3 144 | 14 597 |
2ⁿ | 10 | 16 | 21 | 26 | 31 | 34 | 41 |
n! | 6 | 8 | 10 | 11 | 12 | 13 | 15 |
注:
1)高中学习的时候,约定是ln是以e为底,lg是以10为底,log是必须标定底。在实际应用中,这个约定并没有得到很好的遵守。之后统一区分为:普通应用都是10,计算机学科是2,编程语言里面是e。
2)换底公式:
第二章 算法基础
循环不变式
定义:反映循环体中所有循环变量的变化规律并在循环体执行前后都为真的谓词。类似于
循环不变式主要是用来帮助我们验证算法的正确性。使用循环不变式,我们必须证明下面三条性质:
初始化:循环的第一次迭代之前,它为真。
保持:如果循环的某次迭代之前它为真,那么下次迭代之前它仍为真。
终止:在循环终止时,依旧为真。能为我们证明算法是正确的。
循环不变式和数学归纳法类似,但数学归纳法不需要进行第三步。
插入排序
下面我们以插入排序算法为例,进行循环不变式的证明。
插入排序的算法步骤如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5,直至取完所有元素。
伪代码如下:
该算法的循环不变式为A[1…j-1],下面来证明:
初始化:第一次迭代之前(j=2),数组只有一个元素,显然,此时数组是有序的。
保持:假设A[1…j-1]为有序的,我们进行迭代,插入A[ j ],A[j]会插入到合适的位置,使A[1…j]有序。因此保持性成立。
终止:当j > A.length = n 时,循环终止。此时j = n + 1,所以A[1…j-1]即A[1…n],即整个数组已成功排序。因此算法正确。
第三章 函数的增长
渐近记号
第四章 分治策略
定义
在分治策略中,我们递归的求解一个问题,在每层递归中应用到三个步骤:
分解(Dicide):奖为题划分为一些子问题,子问题的形式和原问题一样,只是规模更小。
解决(Conquer):递归地求解出子问题,如果子问题规模足够小,就停止递归,直接求解。
合并(Combine):奖子问题的解合并成原问题的解。
归并排序
归并排序是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
算法描述:
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
代码实现:
public class MergeSort
{
public static void main(String[] args)
{
int[] array = new int[]{5, 10, 8, 7, 9, 6, 2, 4, 1, 3};
sort(array, 0, array.length - 1);
for (int i = 0; i < array.length; i++)
{
System.out.print(array[i] + " ");
}
System.out.println();
}
public static void sort(int[] array, int left, int right)
{
if (left < right)
{
int mid = (left + right) / 2;
sort(array, left, mid);
sort(array, mid + 1, right);
merge(array, left, mid, right);
}
}
public static void merge(int[] array, int left, int mid, int right)
{
int[] tmpArr = new int[right - left + 1];
//third记录中间数组的索引
int third = 0;
int tmp = left;
int midTmp = mid + 1;
while (left <= mid && midTmp <= right)
{
//从两个数组中取出最小的放入中间数组
if (array[left] <= array[midTmp])
{
tmpArr[third++] = array[left++];
}
else
{
tmpArr[third++] = array[midTmp++];
}
}
//剩余部分依次放入中间数组
while (midTmp <= right)
{
tmpArr[third++] = array[midTmp++];
}
while (left <= mid)
{
tmpArr[third++] = array[left++];
}
//将中间数组中的内容复制回原数组
int i = 0;
while (tmp <= right)
{
array[tmp++] = tmpArr[i++];
}
}
}
最大子数组问题
问题描述:给定一个整数数组A,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
暴力求解法
遍历每对可能买进和卖出的日期组合,只需要保证卖出在买入之后即可,从而找到最大子数组。这种做法的时间复杂度为O(n²)。
代码实现
/**
* 暴力破解
*/
public static int maxSubarray1(int[] nums)
{
int max = Integer.MIN_VALUE;
for (int i = 0; i < nums.length; i++)
{
int maxTmp = 0;
for (int j = i; j < nums.length; j++)
{
// j >= i
// 计算子数组[i,j]的和,并取最大值
maxTmp += nums[j];
if (maxTmp > max)
max = maxTmp;
}
}
return max;
}
分治法
假设我们要找子数组A[low,high]的最大子数组。使用分治法,将子数组再划分为两个规模相同的子数组。找到子数组的中间位置,定为mid,然后求解两个子数组A[low,mid]和A[mid+1,high]。这样,A[low,high]的任意连续子数组A[i,j]所处的位置一定是下面三种情况之一:
- 完全位于子数组A[low,mid]中,即low<=i<=j<=mid;
- 完全位于子数组A[mid+1,high]中,即mid<i<=j<=high;
- 跨越中点mid,即low<=i<=mid<j<=high。
最大子数组也一定是上面三种情况之一。事件负责度:O(nlgn)。
代码实现
/**
* 分治法
*/
public static int maxSubarray2(int[] nums, int left, int right)
{
if (left == right)
return nums[left];
int mid = (left + right) / 2;
int lowSum = maxSubarray2(nums, left, mid);
int highSum = maxSubarray2(nums, mid + 1, right);
int crossSum = findMaxCrossingSubarray(nums, mid, left, right);
if (lowSum >= highSum && lowSum >= crossSum)
{
return lowSum;
}
else if (highSum >= lowSum && highSum >= crossSum)
{
return highSum;
}
return crossSum;
}
/**
* 找到跨越某点的最大子数组
*/
private static int findMaxCrossingSubarray(int[] nums, int mid, int left, int right)
{
int maxLow = Integer.MIN_VALUE;
int maxHigh = Integer.MIN_VALUE;
int maxTmp = 0;
for (int i = mid; i >= left; i--)
{
maxTmp += nums[i];
if (maxTmp > maxLow)
maxLow = maxTmp;
}
maxTmp = 0;
for (int i = mid + 1; i <= right; i++)
{
maxTmp += nums[i];
if (maxTmp > maxHigh)
maxHigh = maxTmp;
}
return (maxLow + maxHigh);
}