您的位置:首页 > 理论基础 > 数据结构算法

读书笔记-----数据结构(排序)

2018-01-18 14:51 197 查看
假设含有n个记录的序列为{r1,r2…rn},其相应的关键字分别为{k1,k2…kn},需确定1,2…n的一种排列p1,p2..pn,使其相应的关键字满足kp1 <=kp2<=….<=kpn非递减(或非递增)关系,即使得序列成为一个按照关键字有序排列的序列{rp1,rp2….rpn},这样的操作就叫做排序。

排序的分类:

1, 稳定排序以及不稳定排序

排序操作不仅可以针对主关键字还可以针对次关键字来进行排序操作,所以排序的记录列表中可能会存在二个或二个以上的关键字相等的记录,排序结果可能会存在不唯一的情况,我们给出了稳定和不稳定排序。

二个相等的记录,未排序前,前者先于后者,排序后前者仍然先于后者,说明是稳定排序,否则,称为不稳定排序。

2,内排序与外排序

根据排序的记录是否全部放置在内存中的情况,分为内排序以及外排序,内排序是指排序的记录全部放置在内存中,外排序是因为排序的记录个数太多,不能同时放置在内存,需要在内外存之间多次交换数据才能进行。

对于内排序而言,排序算法的性能影响因素:

①时间性能(比较以及移动相关)

②辅助空间(除了存放待排序所占用的存储空间外,执行算法所需要的其他存储空间)

③算法复杂度

这里的算法复杂度不是指的是算法的时间复杂度,而是算法本身的复杂度,我们一般根据算法所借助的主要操作,将内排序分为:插入排序,交换排序,选择排序和归并排序。

第一种冒泡排序

在冒泡排序之前的一种交换排序法,依次选择关键字,与后续的关键字来进行比较,如果不满足要求就交换。

在此基础上来进行还进,也就是冒泡排序法

void BubbleSort(SqList *L)
{
int i, j;
for(i = 1; i<L->length; i++)
{
for(j = L->length-1; j>=i; j--)
{
if(L->r[j]>L->r[j+1])
{
swap(L, j, j+1);
}
}
}
}


对上面的冒泡排序算法的改进,避免进行一些多余的比较工作,增加了一个标记变量flag来实现改进。

void BubbleSort2(SqList *L)
{
int i, j;
Status flag = TRUE;
for(i = 1;i<L->length
4000
&& flag;i++)
{
flag = FALSE;
for(j = L->length-1; j>=i; j--)
{
if(L->r[j]>L->r[j+1])
{
swap(L, j, j+1);
flag = TRUE;
}
}
}
}


对于冒泡排序的复杂度的分析,在最好的情况下,我们需要进行排序的记录已经按照顺序来排好了,然后只需要进行n-1次的比较即可,时间复杂度为O(n)。在最坏的情况下,需要进行排序的数据是逆序排列的数据,我们需要进行比较n(n-1)/2次,以及移动同样的次数,此时时间复杂度为O(n^2).

第二种简单选择排序

通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1<=i<=n)个记录进行交换。

void SelectSort(SqList *L)
{
int i,j,min;
for(i = 1;i<L->length;i++)
{
min = i;
for(j = i+1; j<=L->length; j++)
{
if(L->r[min]>L->r[j])
min = j;
}
if(i != min)
swap(L,i,min);
}
}


简单选择排序复杂度分析

简单选择排序的特点就是最大可能的减少交换的次数,但比较的次数并没有减少。分析其时间复杂度而言,都需要进行n-1+n-2+…+1 = n(n-1)/2次比较,对于交换次数,最少进行0次,也就是已经排好序的情况,最多进行n-1次,倒序的情况。总体来说,时间复杂度为O(n^2),与冒泡排序的时间复杂度相同,但总体来说还是优于冒泡排序的。

第三种 直接插入排序

直接插入排序的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的,记录数增1的有序表。

void InsertSort(SqList *L)
{
int i, j;
for(i = 2; i <= L->length; i++)
{
if(L->r[i]<L->r[i-1])
{
L->r[0] = L->r[i];
for(j = i-1;L->r[j]>L->r[0];j--)
L->r[j+1] = L->r[j];
L->r[j+1] = L->r[0];
}
}
}


对直接插入排序算法而言,考虑其时间复杂度,对于已经排好序的数据而言,仅仅需要n-1次比较即可,不需要交换操作。对于最坏的情况,是逆序的情况,我们需要进行比较2+3+…+n = (n+2)(n-1)/2,移动的次数为3+4+…+n+1 = (n+4)(n-1)/2。由概率相同的原则,平均比较和移动次数约为n^2/4,故直接插入排序的时间复杂度为O(n^2),与冒泡排序与简单选择排序的性能要好一点。

第四种希尔排序

使序列分组,然后对组内进行直接插入排序使分组有序,然后最后对分组的所有元素来进行直接插入排序,做到整体有序。

我们分割待排序的记录,目的就是为了减少待排序记录的个数,并使得整个序列向有序的角度来发展。

void ShellSort(SqList *L)
{
int i,j;
int increment = L->length;
do
{
increment = increment/3 + 1;
for(i = increment+1; i <= L->length; i++)
{
if(L->r[i]<L->r[i-increment])
{
L->r[0] = L->r[i];
for(j = i-increment; j>0&&L->r[0]<L->r[i]; j-=increment)
L->r[j+increment] = L->r[j];
L->r[j+increment] = L->r[0];
}
}
}
while(increment>1);
}


希尔排序并不是随便分组后各自排序,而是将相隔某个“增量”的记录组成一个子序列,实现跳跃式移动,使得排序的效率提高。

一般而言,增量的选取非常的关键,究竟选择什么样的增量是一个数学难题,增量序列一般选择dlta[k] = 2^(t-k+1)-1(0<=k<=t<=log2(n+1))时,可以获得比较好的效果。但最后一个增量值一定为1,另外由于记录是跳跃式的移动,希尔排序并不是一个稳定的排序算法。

对于上述的代码,希尔排序的时间复杂度为O(n^(3/2))

第五种 堆排序

堆排序是对于简单选择排序的一种改进,并且这种改进的效果非常的明显。堆是具有下列性质的完全二叉树,每个结点的值都大于或者等于其左右孩子结点的值称为大顶堆,或者每个结点的值都小于或者等于其左右孩子结点的值称为小顶堆。

堆排序就是利用堆(如大顶堆)来进行排序,他的基本思想是:将待排序的序列构造为一个大顶堆,此时,整个序列的最大值就是堆顶的元素,把他移走,然后与最后一个元素进行交换,然后利用n-1个元素来重新构造成一个堆,这样可以得到次大的元素,如此反复执行,最终可以得到一个有序序列。

主要关键问题:如何来维护堆结构,是我们关注的重点问题。

void HeapSort(SqList *L)
{
int i;
for(i = L->length/2; i>0; i--)
HeapAdjust(L,i,L->length);

for(i = L->length; i>1; i--)
{
swap(L,1,i);
HeapAdjust(L,1,i-1);
}
}


void HeapAdjust(SqList *L, int s, int m)
{
int temp,j;
temp = L->r[s];
for(j = 2*s; j<=m; j*=2)
{
if(j<m && L->r[j]<L->r[j
+1])
++j;
if(temp >= L->r[j])
break;
L->r[s] = L->r[j];
s = j;
}
L->r[s] = temp;
}


对于堆排序而言,主要的消耗在于构建堆和重建堆时的反复筛选上。

在构建堆的过程中,我们是完全二叉树从最下层最右边的非终端结点开始构建,将他与其他孩子进行比较和必要的时候的交换,对于每个非终端结点最多进行2次比较和互换操作,因此整个堆的时间复杂度为O(n)。

在正式排序时,第i次取堆顶记录重建堆需要用O(logi)的时间(根据结点到根结点的距离来得到),并且需要取n-1次堆顶记录,因此,重建堆的时间复杂度为O(nlogn)。

所以,总体来说,堆排序的时间复杂度为O(nlogn)。由于堆排序对于原始记录的排序状态并不敏感,所以最好,最坏,平均时间复杂度均为O(nlogn)。

因为堆排序也是一种跳跃式的交换和比较,所以堆排序也是一种不稳定的排序方法。因为初始构建堆时需要比较多的比较次数,所以,并不太适合于数量比较小的排序情况。

第六种 归并排序

归并排序是将二个或者二个以上的有序表组合成一个新的有序表。不断合并的过程,是归并排序的精髓。

void MergeSort(SqList *L)
{
MSort(L->r, L->r, 1, L->length);
}

void MSort(int SR[], int TR1[], int s, int t)
{
int m;
int TR2[MAXSIZE+1];
if(s == t)
TR1[s] = SR[s];
else
{
m = (s+t)/2;
MSort(SR, TR2, s, m);
MSort(SR, TR2, m+1, t);
MSort(TR2, TR1, s, m, t);
}
}

void Merge(int SR[], int TR[], int i, int m, int n)
{
int j, k, l;
if(j = m+1, k=i;i<=m && j<=n; k++)
{
if(SR[i]<SR[j])
TR[k] = SR[i++];
else
TR[K] = SR[j++];
}
if(i<=m)
{
for(l=0; l<=m-i; l++)
TR[k+1] = SR[i+1];
}
if(j<=n)
{
for(l = 0; l<=n-j; l++)
TR[k+1] = SR[j+1];
}
}


归并排序的复杂度分析(时间)

一趟归并需要将待排序的所有的记录扫描一遍,耗费O(n),而整个排序需要进行log2n次,因此,总的时间复杂度为O(nlogn)。对于归并排序算法中,最好,最坏,平均的时间性能。

归并排序的复杂度分析(空间)

归并排序在归并过程中需要与原始记录序列同样数量的存储空间存放归并的结果,以及递归深度为log2n的栈空间,因此空间复杂度为o(n+logn)。

对于归并排序而言,是存在二二比较,不存在跳跃,所以归并排序是一个稳定的排序算法。

但归并排序比较占用内存,但效率比较高且比较稳定的一种排序手段。

对归并排序的一种改进:

利用非递归实现归并排序

void MergeSort2(SqList *L)
{
int *TR = (int*)malloc(L->length*sizeof(int));
int k = 1;
while(k<L->length)
{
MergePass(L->r, TR, k, L->length);
k = 2 * k;
MergePass(TR, L->r, k, L->length);
k = 2 * k;
}
}

void MergePass(int SR[], int TR[], int s, int n)
{
int i = 1;
int j;
while(i <= n-2*s+1)
{
Merge(SR, TR, i, i+s-1, i+2*s-1);
i = i+2*s;
}
if(i<n-s+1)
Merge(SR, TR, i, i+s-1, n);
else
for(j = i; j <= n; j++)
TR[j] = SR[j];
}


第七种 快速排序

希尔排序是直接插入排序的升级,同属于插入排序类,堆排序相当于简单选择排序的升级,同属于选择排序类,而快速排序是对于冒泡排序的升级,相当于交换排序类。

void QuickSort(SqList *L)
{
QSort(L,1,L->length);
}

void QSort(SqList *L, int low, int high)
{
int pivot;
if(low<high)
{
pivot = Partition(L, low, high);
QSort(L, low, pivot-1);
QSort(L, pivot+1, high);
}
}

void Partition(SqList *L, int low, int high)
{
int pivotkey;
pivotkey = L
afbf
->r[low];
while(low<high)
{
while(low<high && L->r[high]>=pivotkey)
high--;
swap(L,low,high);
while(low<high && L->r[low]<=pivotkey)
low++;
swap(L, low, high);
}
return low;
}


快速排序复杂度分析(时间复杂度)

在最优的情况下,每次Partition均可以划分的很均匀,如果排序n个关键字,其递归树的深度为log2n+1,即仅需递归log2n次,第一次需要做n次比较,之后对数组一分为2,需要做n/2次比较,所以,不断的划分下去,有不等式推断。也就是说,最优的情况下,快速排序算法的时间复杂度为O(nlogn)。

在最坏的情况下,也就是正序或者是倒序的情况下,每一次划分只比上一次的划分少一个记录,另外一个记录为空,递归树画出来是一颗斜树,需要执行n-1次递归调用,且每一次划分需要经过n-i次关键字的比较才能找到第i个记录,需要比较n-1+n-2+..+1 = n(n-1)/2,最终的时间复杂度为O(n^2).

对于平均的情况,T(n) = 2/n(T(1)+T(2)+…+T(n))+n

数学归纳法可以推导出,快速排序的时间复杂度为O(nlogn)

快速排序复杂度分析(空间复杂度)

最优的情况下,递归树的深度为log2n,其空间复杂度为O(logn);在最坏的情况下,需要进行n-1次的递归调用,其空间复杂度为O(n);平均情况下,空间复杂度为O(logn).

因为关键字的比较和交换是跳跃的,因此,快速排序是一种不稳定的排序方法。

对于快速排序算法的优化

1,优化选取枢轴

因为枢轴的选取影响到了整个排序算法的优化程度,所以,如何来选取枢轴是我们所关注的问题,最好是让选的枢轴尽可能的位于中间位置,可以三数取中,也可以九数取中。

2,优化不必要的交换

int Partition1(SqList *L, int low, ine high)
{
int pivotkey;
pivotkey = L->r[low];
L->r[0] = pivotkey;
while(low<high)
{
while(low<high && L->r[high]>=pivotkey)
high--;
L->r[low] = L->r[high];
while(low<high && L->r[low]<=pivotkey)
low++;
L->r[high] = L->r[low];
}
L->r[low] = L->r[0];
return low;
}


将原先的交换替换为修改,这样可以避免一些重复的比较,在某种程度上,可以提高效率。

3,优化小数组时的排序方案

在对于小数组的排序时,快速排序效果不好,效率最高的还是直接插入排序,其原因在于快速排序应用了递归排序,在少量数据的时候,初期的比较会比较多,效率比较低。我们可以设置一个阈值,在低于阈值的情况下,我们采用直接插入排序,在阈值之上,我们采用快速排序的手段。

4,优化递归操作

对于递归而言,栈的大小有限,并且每次调用都会占用一定的栈空间,如果能减少递归的时候,我们可以大大的提高性能。

实现尾递归来实现优化。

void QSort1(SqList *L, int low, int high)
{
int pivot;
if((high-low)>MAX_LENGTH_INSERT_SORT)
{
while(low<high)
{
pivot = Partition1(L, low, high);
QSort1(L, low, pivot-1);
low = pivot + 1;
}
}
else
InsertSort(L);
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: