快速排序
基本思想
在待排序的n个元素中任取一个元素(通常取第一个元素)作为基准,把该元素放入最终位置后,整个数据序列被基准分割成两个子序列,所有小于基准的元素放置在前子序列中,所有大于基准的元素放置在后子序列中,并把基准排在这两个子序列的中间,这个过程称作划分。然后对两个子序列分别重复上述过程,直至每个子序列内只有一个记录或空为止。
分治策略
① 分解:将原序列a[s…t]分解成两个子序列a[s…i-1]和a[i+1…t],其中i为划分的基准位置。
② 求解子问题:若子序列的长度为0或为1,则它是有序的,直接返回;否则递归地求解各个子问题。
③ 合并:由于整个序列存放在数组中a中,排序过程是就地进行的,合并步骤不需要执行任何操作。
代码
//通过排序其他元素使得划分元素的最后位置确定
int Partition(int a[], int s, int t)//划分算法
{
int i = s, j = t;
int tmp = a[s];
while (i != j)//从序列两端向中间进行扫描
{
while (j > i&&a[j] >= tmp)//寻找比划分元素小的数
j--;
a[i] = a[j];//将小的数放置到划分元素前
while (i < j&&a[i] <= tmp)//寻找比划分元素大的数
i++;
a[j] = a[i];//将大的数放置到划分元素后
}
a[i] = tmp;//将划分元素最后的位置确定并替换为相应的划分元素
return i;//返回划分元素的位置
}
void QuickSort(int a[], int s, int t)
{
if (s < t)
{
int i = Partition(a, s, t);
QuickSort(a, s, i - 1);//对左子序列递归排序
QuickSort(a, i + 1, t);//对右子序列递归排序
}
}
算法分析
快速排序的时间主要耗费在划分操作上,对长度为n的区间进行划分,共需n-1次关键字的比较,时间复杂度为O(n)。
对n个记录进行快速排序的过程构成一棵递归树,在这样的递归树中,每一层至多对n个记录进行划分,所花时间为O(n)。
当初始排序数据正序或反序时,此时的递归树高度为n,快速排序呈现最坏情况,即最坏情况下的时间复杂度为O(n2);
当初始排序数据随机分布,使每次分成的两个子区间中的记录个数大致相等,此时的递归树高度为log2n,快速排序呈现最好情况,即最好情况下的时间复杂度为O(nlog2n)。快速排序算法的平均时间复杂度也是O(nlog2n)。
归并排序
基本思想
归并排序的基本思想是:首先将a[0…n-1]看成是n个长度为1的有序表,将相邻的k(k≥2)个有序子表成对归并,得到n/k个长度为k的有序子表;然后再将这些有序子表继续归并,得到n/k2个长度为k2的有序子表,如此反复进行下去,最后得到一个长度为n的有序表。
若k=2,即归并在相邻的两个有序子表中进行的,称为二路归并排序。若k>2,即归并操作在相邻的多个有序子表中进行,则叫多路归并排序。
自底向上的二路归并排序算法
分治策略
循环log2n次,length依次取1、2、…、log2n。每次执行以下步骤:
① 分解:将原序列分解成length长度的若干子序列。
② 求解子问题:将相邻的两个子序列调用Merge算法合并成一个有序子序列。
③ 合并:由于整个序列存放在数组中a中,排序过程是就地进行的,合并步骤不需要执行任何操作。
代码
void Merge(int a[], int low, int mid, int high)
{
int *tmpa;//临时数组
int i = low, j = mid + 1, k = 0;
tmpa = (int *)malloc((high - low + 1) * sizeof(int));
while (i <= mid && j <= high)//将两个子表的数据合并到一个数组
{
if (a[i] <= a[j])
{
tmpa[k] = a[i];
i++;
k++;
}
else
{
tmpa[k] = a[j];
j++;
k++;
}
}
//将表中剩余元素添加到临时数组
while (i <= mid)
{
tmpa[k] = a[i];
i++;
k++;
}
while (j <= high)
{
tmpa[k] = a[j];
j++;
k++;
}
//将临时数组的元素复制到原数组
for (k = 0, i = low; i <= high; k++, i++)
{
a[i] = tmpa[k];
}
free(tmpa);
}
void MergePass(int a[], int length, int n)
{
int i;
for (i = 0; i + 2 * length - 1 < n; i = i + 2 * length)//归并length长的两相邻子表
Merge(a, i, i + length - 1, i + 2 * length - 1);
if (i + length - 1 < n)//若有余下的子表则进行归并
Merge(a, i, i + length - 1, n - 1);
}
void MergeSort(int a[], int n)
{
int length;
for (length = 1; length < n; length = 2 * length)
MergePass(a, length, n);
}
算法分析
对于上述二路归并排序算法,当有n个元素时,需要log2n趟归并,每一趟归并,其元素比较次数不超过n-1,元素移动次数都是n,因此归并排序的时间复杂度为O(nlog2n)。
自顶向下的二路归并排序算法
例如,对于{2,5,1,7,10,6,9,4,3,8}序列,说明其自顶向下的二路归并排序的过程。
分治策略
① 分解:将序列a[low…high]一分为二,即求mid=(low+high)/2;递归地对两个子序列a[low…mid]和a[mid+1…high]进行继续分解。其终结条件是子序列长度为1(因为一个元素的子表一定是有序表)。
② 求解子问题:排序两个子序列a[low…mid]和a[mid+1…high]。
③ 合并:与分解过程相反,将已排序的两个子序列a[low…mid]和a[mid+1…high]归并为一个有序序列a[low…high]。
代码
void Merge(int a[], int low, int mid, int high)
{
int *tmpa;//临时数组
int i = low, j = mid + 1, k = 0;
tmpa = (int *)malloc((high - low + 1) * sizeof(int));
while (i <= mid && j <= high)//将两个子表的数据合并到一个数组
{
if (a[i] <= a[j])
{
tmpa[k] = a[i];
i++;
k++;
}
else
{
tmpa[k] = a[j];
j++;
k++;
}
}
//将表中剩余元素添加到临时数组
while (i <= mid)
{
tmpa[k] = a[i];
i++;
k++;
}
while (j <= high)
{
tmpa[k] = a[j];
j++;
k++;
}
//将临时数组的元素复制到原数组
for (k = 0, i = low; i <= high; k++, i++)
{
a[i] = tmpa[k];
}
free(tmpa);
}
void MergeSort(int a[],int low,int high)
//二路归并算法
{ int mid;
if (low<high) //子序列有两个或以上元素
{ mid=(low+high)/2; //取中间位置
MergeSort(a,low,mid); //对a[low..mid]子序列排序
MergeSort(a,mid+1,high); //对a[mid+1..high]子序列排序
Merge(a,low,mid,high); //将两子序列合并
}
}
算法分析
设MergeSort(a,0,n-1)算法的执行时间为T(n),显然Merge(a,0,n/2,n-1)的执行时间为O(n),所以得到以下递推式:
容易推出,T(n)=O(nlog2n)。