插入排序
算法:
插入排序类似于我们打扑克时,每抓一张牌,就按顺序插入到手中已有的有序的牌中。
代码如下:
//对a[0]~a[n-1]排序
void insert_sort(int *a,int n)
{
for(int i=1;i<n;i++)
{
int tmp=a[i];
int j=i-1;
while(tmp<a[j] && j>=0)
{
a[j+1]=a[j];
j--;
}
a[j+1]=tmp;
}
}
对于数组a,tmp左边的这段始终是已排好序的。
举例:
a={8,2,4,9,3,6},n=6,经历n-1个循环之后,a依次变成了:
2,8,4,9,3,6
2,4,8,9,3,6
2,4,8,9,3,6
2,3,4,8,9,6
2,3,4,6,8,9
分析:
现在来分析一下算法的运行时间:
如果数组a本身已经是排好序的了,那么插入排序要做的工作就非常少了,因为它们已经各就各位了。最坏的是逆序的情形,算法必须做大量的工作。
渐近分析
的基本思路:忽略那些依赖于机器的常量以及不是去检查实际的运行时间,而是关注运行时间的增长。
渐近符号:
Θ:对某个公式,弃去它的低阶项,并忽略前面的常数因子(因为n->∞时,低阶项和常数因子都不会影响Θ(n3) > Θ(n2)的结果)
用T(n)表示输入规模为n时的最长运行时间,
T
(
n
)
=
∑
i
=
1
n
−
1
Θ
(
i
)
=
Θ
(
n
2
)
T(n)=\sum_{i=1}^{n-1}{Θ(i)}{=Θ(n^2)}
T(n)=∑i=1n−1Θ(i)=Θ(n2)
(为什么呢?外循环中i从1~n-1,我们把每次循环的工作汇总;内循环在j每次取新值时只做固定量的工作,从0~i-1,所以是Θ(i)的量的工作;然后相当于对连续整数求和,说到底是1+2+3+…+n,这是一种算数级数。)
归并排序
算法:
归并排序的思想就是,如果能把整个输入拆成已经有序(比如升序)的两个序列,把他们归并一下就可以得到一个有序的序列。怎么保证分成的两个序列是有序的呢?通过不断地划分这个序列直到最小粒度, 也就是只有一个数的时候,它一定是有序的, 然后一层一层向上归并,得到最终的有序序列。
归并的过程就不用多说了,就是用两个指针指向两个序列的开始,不断地选两个中较小的,选中的就指针往后移一个,直到某个序列遍历完。
粘上代码:
void merge(int* arr,int p,int q,int r)
{
int n1 = q - p +1;
int n2 = r - q;
int L[n1+1], R[n2+1];
int i, j, k;
for(i = 0; i < n1; i++)
L[i] = arr[p+i];
for(j = 0; j < n2; j++)
R[j] = arr[q+j+1];
L[n1] = R[n2] = (unsigned)(~0) >> 1;
i = j =0;
for(k = 0; k < r - p +1; k++)
{
if(L[i] <= R[j])
{
arr[p+k] = L[i];
i++;
}
else
{
arr[p+k] = R[j];
j++;
}
}
}
void merge_sort(int *arr,int p,int r)
{
if(r > p)
{
int q = (p + r) / 2;
merge_sort(arr, p, q);
merge_sort(arr, q + 1, r);
merge(arr, p, q, r);
}
}
分析:
我们分析一下整体的复杂度。首先归并的过程的复杂度是Θ(n)。如果序列只有一个元素,复杂度是Θ(1),否则后面两个递归是2T(n/2)。也就是说:
T
(
n
)
=
{
Θ
(
1
)
n
=
1
2
T
(
n
/
2
)
+
Θ
(
n
)
n
>
1
T(n) = \begin{cases} Θ(1) & n=1 \\ 2T(n/2)+Θ(n) & n>1 \end{cases}
T(n)={Θ(1)2T(n/2)+Θ(n)n=1n>1
Θ(1) 比Θ(n) 低阶,对递归没有影响,可以直接舍去。我们可以用构造递归树法求这个复杂度,为Θ(nlogn)。
考虑渐进的情况下,要比插入排序好。