从上面的插入排序思想中,不难得到一种简单直接的插入排序算法。假设待排序表在某次过程中属于这种情况。
|有序序列\(L[1\ldots i-1]\)|L(i)|无序序列\(L[i+1\ldots n]\)|
|:-|:-|
为了实现将元素\(L(i)\)插入到已有序的子序列\(L[1\ldots i-1]\)中,我们需要执行以下操作(为了避免混淆,下面用\(L[]\)表示一个表,而用\(L()\)表示一个元素):
- 查找出\(L(i)\)在\(L[i+1\ldots n]\)中的插入位置k。
- 将\(L[k\ldots i-1]\)中所有元素全部后移一个位置。
- 将\(L(i)\)赋值到\(L(k)\)
void InserSort(int A[],int n)
{
int i,j;
for(i=2;i<=n;i++)
{
if(A[i]<A[i-1])
{
A[0]=A[i];
for(j=i-1;A[0]<A[j];j--)
A[j+1]=A[j];
A[j+1]=A[0];
}
}
}
从前面的直接插入排序算法中,不难看出每趟插入的过程,都进行了两项工作:
- 从前面的子表中查找出待插入元素应该被插入的位置。
- 给插入位置腾出空间,将待插入元素复制到表中的插入位置。
注意到该算法中,总是边比较边移动元素,下面将比较和移动操作分离开,即先折半查找出元素的待插入位置,然后再同意的移动待插入位置之后的元素。
void InserSort(int A[],int n)
{
int i,j,low,high,mid;
for(i=2;i<=n;i++)
{
A[0]=A[i];
low=1,high=i-1;
while(low<=high)
{
mid=(low+high)/2;
if(A[mid]>A[0])
high=mid-1;
else
low=mid+1;
}
for(j=i-1;j>=high+1;j--)
A[j+1]=A[j];
A[high+1]=A[0];
}
}
从前面的代码原理中不难看出,直接插入排序适用于基本有序的排序表和数据量不大的排序表。1959年\(D.L.Shell\)提出了希尔排序,又称为缩小增量排序。
希尔排序的基本思想是:先将待排序表分割为若干个形如\(L[i,i+d,i+2d,\ldots,i+kd]\)的特殊子表,分别进行直接插入排序。希尔排序的排序过程如下:
先取一个小于n的步长d1,把表中全部记录分为\(d_1\),所有距离为\(d_1\)的倍数的记录放在同一个组中,在各组中进行直接插入排序;然后取第二个步长\(d_2\leq d_1\),重复上述过程,直到所取到的\(d_t=1\),即所有记录已放在同一组中,再进行直接插入排序,由于此时已经具有较好的局部有序性,故可以很快的得到结果。到目前为止,尚未求得一个最好的增量序列,希尔提出的方法是\(d_1=\frac n 2\),\(d_{i+1}= \frac {d_i} 2\),并且最后一个增量等于1。
void SheelSort(int A[],int n)
{
for(int dk=n/2;dk>=1;dk=dk/2)
for(int i=dk+1;i<=n;i++)
if(A[i]<A[i-dk])
{
A[0]=A[i];
for(int j=i-dk;j>0&&A[0];j=j-dk)
A[j+dk]=A[j];
A[j+dk]=A[0];
}
}
冒泡排序的算法思想是:假设待排序表长为n,从后往前(或者从前向后)两两比较相邻元素的值,若为逆序(即\(A[i-1]>A[i]\)),则交换他们,知道序列比较完。我们称之为一趟冒泡,结果将最小的元素交换到待排序的第一个位置(关键字最小的元素如气泡一般逐渐向上“漂浮”直至“水面”,这就是冒泡排序名字的由来)。下一趟冒泡的时候,前一趟确定的最小元素不再参加比较,待排序列减少一个元素,每趟冒泡的结果把序列中的最小元素放到了序列的最终位置。
void BuuleSort(int A[],int n)
{
for(int i=0;i<n-1;i++)
{
bool flag=false;
for(int j=n-1;j>i;j--)
if(A[j-1]>A[j])
{
swap(A[j-1],A[j]);
flag=true;
}
if(flag==false)
break;
}
}
快速排序是对冒泡排序的一种改进,其基本思想是基于分治法的:在待排序表\(L[1\ldots n]\)中任取一个元素pivot作为基准,通过一趟排序将待排序表划分为独立的两部分\(L[1\ldots k-1]\)和\(L[k+1\ldots n]\)使得\(L[1\ldots k-1]\)中所有元素小于pivot,\(L[k+1\ldots n]\)中所有元素均大于或等于pivot,则pivot放在了其最终位置\(L(k)\)上,这个过程称为一趟快速排序。而后分别递归的对两个子表重复上述过程,直至每部分内只有一个元素或者为空为止,即所有元素放在了其最终位置之上。
首先假设划分算法已知,记为\(Partition()\),返回的是上述的k,注意到\(L(k)\)已经在最终的位置,所以可以先对表进行划分,而后对两个表调用同样的排序操作。因此可以递归的调用快速排序算法进行排序,具体的程序结构如下:
int Partition(int A[],int low,int high) // 传入 数组和 上下限
{
int pivot = A[low]; // 让pivot暂存传入的数组第一个值.
while(low<high) // 当low等于high的时候 才跳出去。
{
while(low<high&&A[high]>=pivot) // 当low小于high并且数组靠前的值
high--; //开始 减小high的值 知道不符合上述条件
A[low]=A[high]; // 较小的值 放到前面 那个空位上
while(low<high&&A[low]<=pivot) // 当low小于high并且数组靠前的值比较小
low++;
A[high]=A[low];
}
A[low]=pivot;
return low;
}
void QuickSort(int A[],int low,int high)
{
if(low<high)
{
int pivot = Partition(A,low,high);
QuickSort(A,low,pivot-1);
QuickSort(A,pivot+1,high);
}
}
从上面选择排序的思想中可以很直观的得出简单选择排序算法的思想:假设排序表\(L[1\ldots n]\),第i趟排序即从\(L[i\ldots n]\)中选择关键字最小的元素与\(L(i)\)交换,每一趟排序可以确定 一个元素的最终位置,这样经过\(n-1\)趟排序就可以使整个排序表有序。
void SelectSort(int A[],int n)
{
for(int i=0;i<n-1;i++)
{
int Min=i;
for(int j=i+1;j<n;j++)
if(A[j]<A[Min])
Min=j;
if(Min!=i)
swap(A[i],A[Min]);
}
}
堆排序是一种树形选择排序方法,它的特点是:在排序过程中,将\(L[1\ldots n]\)看做一棵完全二叉树的顺序存储结构,利用完全二叉树双亲结点和孩子结点之间的内在关系,在当前无序区中选择关键字最大。
堆的定义如下:n个关键字序列\(L[1\ldots n]\)称为堆,当且仅当该序列满足:\(L(i)\leq L(2i)\)且\(L(i)\leq L(2i+1)\)或\(L(i)\geq L(2i)\)且\(L(i)\geq L(2i+1)\)。
满足前者的称为小根堆(小顶堆),满足后者情况的堆称为大根堆(大顶堆)。显然,在大根堆中,最大元素存放在根节点中,且对其任一费根节点,它的值小于或者等于其双亲结点值。小根堆的定义刚好相反,根节点是最小元素。下图所示为一个大根堆。
堆排序的关键是构造初始堆,对初始序列建堆,就是一个反复筛选的过程。n个结点的完全二叉树,最后一个结点是第 \([\frac n 2]\)个结点为根的子树筛选(对于大根堆,若根节点的关键字小于左右子女中较大者,则交换),使该子树成为堆。之后向前依次对各结点 \(([\frac n 2]-1 \rightarrow 1)\)为根的子树进行筛选,看该节点值是否大于其左右子结点的值,若不是,将左右子结点中较大值与之交换,交换后可能会破坏下一级的堆,遇事继续采用上述方法构造下一级的堆,直到以该节点为根的子树构成堆为止。反复利用上述调整对的方法建立堆,知道根节点。过程如下图所示:
void AdjustDown(int A[],int k,int len)
{
A[0]=A[k];
for(int i=2*k;i<=len;i=i*2)
{
if(i<len&&A[i]<A[i+1])
i++;
if(A[0]>=A[i])
break;
else
{
A[k]=A[i];
k=i;
}
}
A[k]=A[0];
}
void AdjustUp(int A[],int k)
{
A[0]=A[k];
int i=k/2;
while(i>0&&A[i]<A[0])
{
A[k]=A[i];
k=i;
i=k/2;
}
A[k]=A[0];
}
void BuildMaxHeap(int A[],int len)
{
for(int i=len/2;i>0;i--)
AdjustDown(A,i,len);
//AdjustUp(A,i);
}
void HeapSort(int A[],int len)
{
BuildMaxHeap(A,len);
for(int i=len;i>1;i--)
{
swap(A[i],A[1]);
AdjustDown(A,1,i-1);
}
}
向下调整的时间和树的高度有关,为\(O(h)\)。建堆过程中每次向下调整时,大部分结点的高度都比较小。因此,可以证明在元素个数为n的序列上建堆,其时间复杂度\(O(n)\),这说明可以在线性时间内,将一个无需数组建立成一个大顶堆。
应用堆这种数据结构进行排序的思路很简单,首先将存放在\(L[1\ldots n]\)中的n个元素简称初始堆,由于堆本身的特点(以大顶堆)为例,堆顶元素就是最大值,输出堆顶元素后,通常将堆低元素送入堆顶,此时根节点已经不满足于大顶堆的性质,堆被破坏,将对顶元素向下调整使其继续保持大顶堆的性质,再输出堆顶元素。如此重复,直到堆中仅剩下一个元素为止。
归并排序和上述基于交换,选择等排序思想不一样,“归并”的含义是将两个或者两个以上的有序表组合为一个新的有序表。假定待排序表中含有n个记录,则可以看为是n个有序的子表,每个子表长度为1,然后两两归并,得到\([\frac n 2]\)个长度为2或者1的有序表;再两两归并,如此这般 重复归并,知道合并为一个长度为n的有序表为止,这种排序方法称为2-路归并排序。
\(Merge()\)的功能是将前后相邻的两个有序表归并为一个有序表的算法。设两段有序表 \(A[low\ldots mid]\), \(A[mid+1\ldots high]\)存放在同一顺序表中相邻的位置上,先将他们复制到辅助数组B中。每次从对应B中的两段去除一个记录进行关键字的比较,将较小者放入A中,当数组B中有一段的下标超过其对应的表长时(即改段的所有元素已经完全复制到A中),将另一段中的剩余部分直接复制到A中。