2.1 插入排序
核心思想: 依次从未排序的数组中取出数,并将该数插入到有序数组中,并保持有序数组依然有序。(初始状态,有序数组仅一个数开始)—算法导论里给出的形象比喻:左手扑克牌已排好序,右手扑克牌未排序,从右手依次从左到右取出牌放入左手牌中并保证左手的拍依旧有序。
C#代码(升序):
public void InsertSortUp(List<int> array)
{
int key, j; //key用来代表取出的值
for (int i = 1; i < array.Count; i++)
{
key = array[i];
//insert array[i] into the sorted array[1...j-1]
j = i - 1;
while (j >= 0 && array[j] > key)
{
array[j + 1] = array[j];
j--;
}
array[j + 1] = key;
}
}
Note: 以上易错在于while循环部分将大于key的后移一位。
Exercise:
2.1-1
<31><41,59,26,41,58>→<31,41><59,26,41,58>→<31,41,59><26,41,58>→<26,31,41,59><41,58>→<26,31,41,41,59><58>→<26,31,41,41,58,59>
2.1-2
将上述升序代码中的array[j]>key改为array[j]<key便可实现降序排序
2.1-3
线性查找问题:
//if num=array[i],return i, else return -1;
public int LSearch(List<int> array,int num)
{
for(int i=0;i<array.Count;i++)
{
if (num == array[i])
return i;
}
return -1;
}
2.1-4
二进制数相加问题:
//A+B=C (均为二进制数以及A,B的长度相等,不足用0添)
public int[] Add(int[] A,int[] B,int n)
{
int temp=0,sum=0;
int[] C=new int[n+1];
for(int i=n-1;i>=0;i--)
{
sum = A[i] + B[i]+temp; //temp用于代表进位值
if (sum >= 2) //存在进位情况
{
C[i + 1] = sum - 2;
temp = 1;
}
else //不存在进位情况
{
C[i+1] = sum;
temp = 0;
}
}
C[0] = temp; //C比A和B多一位(该位也可能为0)
return C;
}
**Note:二进制存入数组是越高位处于数组下标越小处。此外,可进一步改善为对任意二进制均适用的加法。**
2.2 分析算法
Exercise:
2.2-1
n3/1000−100n2−100n+3
表示:θ(n3)
2.2-2
选择算法(依次找出最小的”往前扔”):
public void Selectsort(List<int> array)
{
int min, pos;
for (int i = 0; i < array.Count-1; i++)
{
min = array[i]; //min用于存储最小值
pos = i; //pos用于存储最小值的下标
for (int j = i+1; j < array.Count; j++)
{
if (array[j] < min)
{
min = array[j];
pos = j;
}
}
array[pos] = array[i];
array[i] = min;
}
}
最好情况最坏情况运行时间均为:θ(n2)
2.2-3
线性查找问题(要查找元素等可能地为数组中的任意元素):
平均需检查输入序列元素数: (1+n)(n/2)/n=(n+1)/2
最坏情况需检查输入序列元素数:n
平均情况与最坏情况均为 θ(n)
2.2-4
尽量减少循环次数
2.3 设计算法
分治模式的三个步骤:
- 分解:将原问题分解成若干子问题,这些子问题均为原问题规模较小的实例。
- 解决:递归地求解各子问题,当子问题规模足够小时直接求解。
- 合并:合并这些子问题的解成原问题的解。
归并排序:
合并(将两个规模小的有序数组合并为大的有序数组)
//含有哨兵的情况,arr1[n1]和arr2[n2]均为哨兵
private void Merge(List<int> array,int begin,int mid,int end)
{
int n1 = mid - begin+1;
int n2 = end - mid; //输入的end代表数组最后一项而非数组长度
int[] arr1 = new int[n1+1];
int[] arr2 = new int[n2+1];
for (int i = 0; i < n1; i++) //赋值
arr1[i] = array[begin+i];
for (int i = 0; i < n2; i++)
arr2[i] = array[mid+i+1];
arr1[n1] = Int16.MaxValue;
arr2[n2] = Int16.MaxValue;
int m = 0, n = 0;
for (int k = begin; k <=end; k++) //依次取出两数组中较小值,此处为核心
{
if (arr1[m] < arr2[n])
{
array[k] = arr1[m];
m++;
}
else
{
array[k] = arr2[n];
n++;
}
}
}
分解+解决(将n分解为两个n/2并递归地排列两个子序列):
public void Mergesort(List<int> array,int begin,int end)
{
int mid;
if(begin<end) //直到每个子数组均只有单个值时停止分解进行求解
{
mid = (begin + end) / 2;
Mergesort(array, begin, mid);
Mergesort(array, mid+1, end);
Merge(array, begin, mid, end);
}
}
Exercise:
2.3-1
2.3-2
不使用哨兵的合并程序:
//无哨兵情况,利用continue语句.
private void Merge2(List<int> array, int begin, int mid, int end)
{
int n1 = mid - begin + 1;
int n2 = end - mid;
int[] arr1 = new int[n1];
int[] arr2 = new int[n2];
for (int i = 0; i < n1; i++)
arr1[i] = array[begin + i];
for (int i = 0; i < n2; i++)
arr2[i] = array[mid + 1 + i];
int m = 0, n = 0;
for (int k = begin; k <= end; k++)
{
if (m == n1) //此判断语句来取代哨兵
{
array[k] = arr2[n];
n++;
continue;
}
if (n == n2)
{
array[k] = arr1[m];
m++;
continue;
}
if (arr1[m] < arr2[n])
array[k] = arr1[m++];
else
array[k] = arr2[n++];
}
}
2.3-3
数学归纳法:
step1: 当n=2时,左边(T(2)=2)=右边(T(2)=2lg2=2)成立。
step2: 不妨假设n=2k时 T(n)=nlgn成立。
step3: 当n=2k+1时,T(n)=2T(n/2)+n=2T(2k)+2k+1=2∗2klg2k+2k+1=(k+1)2k+1=nlgn,所以成立,得证!
2.3-4
插入排序的递归描述:
//note:此处的end代表的是数组的长度
public void InsertSort(List<int> array,int begin,int end)
{
if(begin<end)
{
end--;
InsertSort(array, begin, end);
int key=array[end],i;
for(i=end-1;i>=begin;i--)
{
if (array[i] > key)
array[i + 1] = array[i];
else
break;
}
array[i+ 1] = key;
}
}
2.3-5
二分查找:
public int BinarySearch(List<int> array,int begin,int end,int key)
{
int mid=(begin+end)/2;
while(mid!=begin&&mid!=end)
{
if (array[mid] == key)
return mid;
else if (array[mid] < key)
begin = mid;
else
end = mid;
mid = (begin + end) / 2;
}
return -1; //-1代表没有找到
}
最坏运行时间:假设 2m=n,则最多只能进行m=lgn次对分,所以最坏运行时间θ(lgn)
2.3-6
不可以,由于每次要将比key大的数后移一位,所以不可避免的需要进行m次移动。
2.3-7
n个整数的集合S和另一个整数x,确定S中是否存在两个其和刚好为x的元素: 且要求运行时间为θ(nlgn)—采用归并排序+二分查找
二分查找部分:
private int FindKey(List<int> array,int begin,int end,int key)
{
int mid=(begin+end)/2;
while(mid!=begin&&mid!=end)
{
if (array[mid] == key)
return mid;
else if (array[mid] < key)
begin = mid;
else
end = mid;
mid = (begin + end) / 2;
}
return -1;
}
归并+整体查找
public List<int> FindNum(List<int> array,int num)
{
MergeSort sort = new MergeSort(); //归并排序
sort.Mergesort(array, 0, array.Count - 1);
int key;
int[] result = new int[array.Count];
List<int> res=new List<int>();
for(int i=0;i<array.Count;i++)
{
key = num - array[i];
result[i]=FindKey(array,0,array.Count-1,key);
}
for(int i=0;i<result.Length/2;i++) //为了避免重复采取的措施
{
if (result[i] != -1&&res.Contains(result[i])==false)
res.Add(i);
}
return res;
}
Note: 还有更精妙的解法(但相对需要数学知识)
思考题
2-1 归并排序+插入排序
在归并排序中对小数组采用插入排序—-使用插入排序来排序长度为k的n/k个子表。
插入排序(同文章开头的插入排序)
归并排序中的合并(采用上述中无哨兵的合并)
具体的分解+解决:
public void MSort(List<int> array, int begin, int end,int k)
{
int mid = (begin + end) / 2; ;
if ((end-begin)>k) //当两者的间距小于等于k时采用插入排序
{
MSort(array, begin, mid,k);
MSort(array, mid + 1, end,k);
}
else
InsertSort(array, begin, end);
Merge2(array, begin, mid, end);
}
a. 证明:每个长度为k的子序列进行插入排序需要时间 (k−1)+(k−2)+…+1=k(k−1)2 从而n/k个长度为k的子表需要时间为 k(k−1)2nk=n(k−1)2=θ(nk)
b. 从n个数到n/k个k长度的数需要划分次数为:n2m=k→m=lgnk,即共有lgnk层,而每层所需时间为n,所以合并这些子表所需时间为 θ(nlgnk).
c. 易知θ(nlgnk)<θ(nlgn),因此,只需θ(nk)<θ(nlgn)便可,所以k<lgn。
d. k的选择,当输入数据较好时,插入排序耗时少k选择大,否则k选择小。
2-2 冒泡排序
public void Bubblesort(List<int> array)
{
int temp;
for(int i=0;i<array.Count;i++)
for(int j=array.Count-1;j>i;j--)
{
if(array[j]<array[j-1])
{
temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
冒泡排序的最坏情况运行时间:θ(n2).
2-3 略
2-4 逆序对
定义: A[1…n]是一个有n个不同数的数组,若i<j,且A[i]>A[j],则对偶(i , j)称为A的一个逆序对。
a. <2, 3, 8, 6, 1>的5个逆序对(2, 1), (3, 1), (8, 6), (8, 1), (6, 1)
b. 由集合{1, 2, …, n}中的元素构成的降序排序数组具有最多的逆序对,有n(n−1)2对。
c. 成正比,插入时间越长,逆序对的数量越多(此处插入排序是指升序排序)
d. 求逆序对数量的算法:合并时即进行排序同时记录每个子例中逆序对个数。
//归并算法求每个子数组中逆序对个数并进行排序
private int Merge2(List<int> array, int begin, int mid, int end)
{
int n1 = mid - begin + 1;
int n2 = end - mid;
int num = 0;
int[] arr1 = new int[n1];
int[] arr2 = new int[n2];
for (int i = 0; i < n1; i++)
arr1[i] = array[begin + i];
for (int i = 0; i < n2; i++)
arr2[i] = array[mid + 1 + i];
int m = 0, n = 0;
for (int k = begin; k <= end; k++)
{
if (m == n1)
{
array[k] = arr2[n];
n++;
continue;
}
if (n == n2)
{
array[k] = arr1[m];
m++;
continue;
}
if (arr1[m] < arr2[n])
array[k] = arr1[m++];
else
{
array[k] = arr2[n++];
num = num+(n1-m); //记录逆序对个数
}
}
return num;
}
//获取逆序对总数量
public void MSort(List<int> array, int begin, int end)
{
int mid;
if (begin < end)
{
mid = (begin + end) / 2;
MSort(array, begin, mid);
MSort(array, mid + 1, end);
int num=Merge2(array, begin, mid, end);
result += num;
}
}
//返回结果
private static int result;
public int Getresult()
{
return result;
}