Matrix Wall

简单分治

在之前的排序算法中,我们提到了一种叫归并排序的算法,就是通过把待排序的数列分成一个个小的数列并排序成有序表,再将这些小的有序表组合成一个新的有序表来完成排序,所以归并排序算法就是利用分治策略的一个很好的典型。

概念阐述

在分治策略中我们用递归的方法来求解一个问题,在每一层的递归中应用如下的三个步骤:

  • 分解(Dived)步骤将问题划分为一些子问题,子问题的形式与原问题一样,只是规模更小。
  • 解决(Conquer)步骤递归地求解出子问题。如果子问题的规模足够小,则停止递归,直接求解。
  • 合并(Combine)步骤将子问题的解组合成原问题的解。

简单通俗地来说,所谓的分治策略就是把很复杂的问题用递归的方式细化成一个个小的,能够解决子问题,再把所有子问题的解合并起来,就得到原问题的解。

下面就通过一个例子来简单介绍分治策略。

实例问题

这里有一个数组A:

[13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]

现在要求寻找数组A中和最大的非空连续子数组(我们称这样的连续子数组为最大子数组)。

暴力求解法

乍一看到这个问题,首先想到的当然就是简单粗暴地套上双重循环,尝试每对可能的组合,再进行比较,得出最终结果,这就是我们常说的暴力求解法,算法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//最大子数组问题:暴力求解法
#include <stdio.h>
int main()
{
int A[] = { 13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, 22, 15, -4, 7 };
int length = sizeof( A ) / sizeof( int );
int max = -1000;
int i, j, k, sum, step, left;
for( i = 1; i <= length; i++ )
{
for( j = 0; ( j + i -1 ) <= length-1; j++ )
{
sum = 0;
for( k = j; k <= j + i - 1; k++ )
sum += A[ k ];
if( sum > max )
{
max = sum;
left = j;
step = i;
}
}
}
printf("length:%d, max:%d, left:%d, right:%d, step:%d\n", length, max, left, left+step-1, step);
getchar();
return 0;
}
```

---

但是套上两个循环后,算法的时间复杂度就变为了O(n²),我们还有更好的方法。

---

## 分治策略求解法

现在我们假设要寻找子数组`A[low..high]`的最大子数组,使用分治就意味着要把数组A分成两个规模尽量相等的子数组,也就是说找到数组的中央位置mid,然后考虑求解两个子数组`A[low..mid]`和`A[mid+1..high]`。这时的最大子数组`A[i..j]`所在的位置必然是如下三处:
- 完全处于子数组`A[low..mid]`中,因此 `low ≤ i ≤ j ≤ mid`。
- 完全处于子数组`A[mid+1..high]`中,因此 `mid+1 ≤ i ≤ j ≤ high`。
- 跨越了中点mid,因此 `low≤ i ≤ mid < j ≤ high`。

接下来我们就可以递归地求解数组`A[low..mid]`和`A[mid+1..high]`的最大子数组,因为这两个子问题仍然是最大子数组问题,只是规模更小,因此剩下的子工作就是在这三个子数组的最大子数组中找出最大的一个。

任何跨中点的最大子数组都由两个子数组`A[i..mid]`和`A[mid+1..j]`组成,其中low≤ i ≤ mid且mid < j ≤ high,因此只要找到形如`A[i..mid]`和`A[mid+1..j]`的最大子数组,然后将其合并即可,对此算导给出了如下的伪代码:

FIND-MAX-CROSSING-SUBARRRY( A, low, mid, high )
left-sum = -100000
sum = 0
for i = mid downto low
sum = sum + A[i]
if sum > left-sum
left-sum = sum
max-left = i

right-sum = -100000
sum = 0
for j = mid + 1 to high
    sum = sum + A[i]
    if sum > right-sum
        right-sum = sum
        max-right = j

return (max-left, max-right, left-sum + right-sum)
1
2
3
4
5
6

---

有了线性时间的FIND-MAX-CROSSING-SUBARRRY,我们就可以设计求解最大子数组问题的分治算法的伪代码:

---

FIND-MAXIMUM-SUBARRAY(A, low, high)
if high == low
return ( low, high, A[low] );
else
mid = ( low + high ) / 2
( left-low, left-high, left-sum ) =
FIND-MAXIMUM-SUBARRAY( A, low, mid )
( right-low, right-high, right-sum ) =
FIND-MAXIMUM-SUBARRAY( A, mid + 1, high )
( cross-low, cross-high, cross-sum ) =
FIND-MAX-CROSSING-SUBARRRY( A, low, mid, high )
if left-sum >= right-sum and left-sum >= cross-sum
return ( left-low, left-high, left-sum )
elseif right-sum >= left-sum and right-sum >= cross-sum
return ( right-low, right-high, right-sum )
else
return ( cross-low, cross-high, cross-sum )

1
2
3
4
5
6

---

具体实现代码如下:

---

//最大子数组问题:分治策略法

#include <stdio.h>
int last_low, last_high;

int
find_max_cro_arr( int A[], int low, int mid, int high )
{
int i, j;
int left_sum = -100000;
int sum = 0;
for( i = mid; i >= low; i– ){
sum += A[ i ];
if( sum > left_sum )
{
left_sum = sum;
last_low = i;
}
}

int right_sum = -100000;
sum = 0;
for( j = mid + 1; j <= high; j++ ){
    sum = sum + A[ j ];
    if( sum > right_sum )
    {
        right_sum = sum;
        last_high = j;
    }    
}
return left_sum + right_sum;

}

int
find_max_arr( int A[], int low, int high )
{
int left_sum, right_sum, cross_sum;
if( high == low )
return A[ low ];
else
{
int mid = ( low + high ) / 2;
left_sum = find_max_arr( A, low, mid );
right_sum = find_max_arr( A, mid + 1, high );
cross_sum = find_max_cro_arr( A, low, mid, high );
if ( left_sum >= right_sum && left_sum >= cross_sum )
return left_sum;
else if ( right_sum >= left_sum && right_sum >= cross_sum )
return right_sum;
else
return cross_sum;

}

}

int
main()
{
int A[] = { 13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7 };
int length = sizeof( A ) / sizeof( int );
int sum = find_max_arr( A, 0, length - 1 );
printf(“max_sum:%d, low:%d, high:%d\n”, sum, last_low, last_high);

getchar();
return 0;

}
`

参考资料:《算法导论:第三版》。

排序算法(二)

上一篇介绍了几种简单的排序算法,这一篇来谈谈归并排序快速排序

归并排序(Merge Sort)

顾名思义,归并算法就是将两个或两个以上的有序表组合成一个新的有序表,所以归并排序算法是递归算法的一个很好的实例。

算法原理

归并操作的过程如下:

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  4. 重复步骤3直到某一指针达到序列尾
  5. 将另一序列剩下的所有元素直接复制到合并序列尾

举个例子吧:

[ 10 4 6 3 8 2 5 7 ]
[ 10 4 6 3 ] [ 8 2 5 7 ]
[ 10 4 ] [ 6 3 ] [ 8 2 ] [ 5 7 ]
[ 10 ] [ 4 ] [ 6 ] [ 3 ] [ 8 ] [ 2 ] [ 5 ] [ 7 ]
[ 4 10 ] [ 3 6 ] [ 2 8 ] [ 5 7 ]
[ 3 4 6 10 ] [ 2 5 7 8 ]
[ 2 3 4 5 6 7 8 10 ]

算法分析

  • 最差时间复杂度:O(nlogn)

  • 最优时间复杂度:O(n)

  • 平均时间复杂度:O(nlogn)

算法稳定性

归并排序是一种稳定的排序方法

算法实现

//归并排序
typedef int ElemType;

void Merge( ElemType A[], ElemType TmpArray[], int Lpos, int Rpos, int RightEnd );
void MSort( ElemType A[], ElemType TmpArray[], int Left, int Right );
void Mergesort( ElemType A[], int N );

void Merge( ElemType A[], ElemType TmpArray[], int Lpos, int Rpos, int RightEnd )
{
    int i, LeftEnd, NumElements, TmpPos;
    LeftEnd = Rpos - 1;
    TmpPos = Lpos;
    NumElements = RightEnd - Lpos + 1;

    while( Lpos <= leftend="" &&="" rpos="" <="RightEnd" )="" if( A[ Lpos ] <= a[="" rpos="" ]="" )="" tmparray[="" tmppos++="" lpos++="" ];="" else
            TmpArray[ TmpPos++ ] = A[ Rpos++ ];

    while( Lpos <= leftend="" )="" tmparray[="" tmppos++="" ]="A[" lpos++="" ];="" while( Rpos <= rightend="" )="" tmparray[="" tmppos++="" ]="A[" rpos++="" ];="" for( i = 0; i < NumElements; i++, RightEnd-- )
        A[ RightEnd ] = TmpArray[ RightEnd ];
}

void MSort( ElemType A[], ElemType TmpArray[], int Left, int Right )
{
    int Center;

    if( Left < Right )
    {
        Center = ( Left + Right ) / 2;
        MSort( A, TmpArray, Left, Center );
        MSort( A, TmpArray, Center + 1, Right );
        Merge( A, TmpArray, Left, Center + 1, Right );
    }
}

void Mergesort( ElemType A[], int N )
{
    ElemType *TmpArray;

    TmpArray = malloc( N * sizeof( ElemType ));
    if( TmpArray != NULL)
    {
        MSort( A, TmpArray, 0, N - 1 );
        free( TmpArray );
    }
    else
        printf("No space for tmp array!!!");
}

虽然归并排序的运行时间是O(nlogn),但是它很难用于主存排序,主要问题在于合并两个排序的表需要线性附加内存,在整个算法中还要花费将数据拷贝到临时数组再拷贝回来这样一些附加的工作,其结果严重放慢了排序的速度。

归并排序不仅可以用分治的策略来递归处理,也可以用迭代的方法来表示,但即使这样,对于重要的内部排序而言,人们还是选择快速排序。

快速排序(Quick Sort)

正如它的名字所标示的,快速排序是在实践中已知的最快排序算法,该算法特别快,主要是由于非常精炼和高度优化的内部循环。像归并排序一样,快速排序也是一种分治的递归算法。

算法原理

将数组S排序的基本算法由下列简单的4步组成:

  1. 如果S中元素个数是0或1,则返回。
  2. 取S中任一元素,称之为枢纽元。
  3. 将A = S - {v}(S中其余元素)分成两个不相交的集合:S1 = { x属于A | x <= v } 和S2= { x属于A | x >= v }。
  4. 返回{quicksort(S1)后,继随v,继而quicksort(S2)}.

下面给出一个乱序的数组{[ 50 10 90 30 70 40 80 60 20 },以第一个元素50为枢纽元,来展现快速排序的过程。

[ (50) 10 90 30 70 40 80 60 (20) ] 第1次交换

[ (20) 10 90 30 70 40 80 60 (50) ] 第2次交换

[ 20 10 (50) 30 70 40 80 60 (90) ] 第3次交换

[ 20 10 (40) 30 70 (50) 80 60 90 ] 第4次交换

[ 20 10 40 30 (50) (70) 80 60 90 ] 第5次交换

[ 20 10 40 30 (50)70 80 60 90 ] 第6次交换

算法分析

  • 最差时间复杂度:O(n²)
  • 最优时间复杂度:O(nlogn)
  • 平均时间复杂度:O(nlogn)

算法稳定性

快速排序是一种稳定的排序方法

算法实现

typedef typedef int ElemType;

void swap(int A[], int a, int b );
void Quicksort( ElemType A[], int N );
void QSort(int A[], int low, int high);
int Partition(int A[], int low, int high );

void Quicksort( ElemType A[], int N )
{
    QSort( A, 0, N - 1 );
}

void swap(int A[], int a, int b )
{
    int Tmp;
    Tmp = A[ a ];
    A[ a ] = A[ b ];
    A[b] = Tmp;
}

void QSort(int A[], int low, int high)
{
    int pivot;

    if( low < high )
    {
        pivot = Partition( A, low, high );
        QSort( A, low, pivot-1 );
        QSort( A, pivot+1, high );
    }
}

int Partition(int A[], int low, int high )
{
    int pivotkey;
    pivotkey = A[ low ];
    while( low < high )
    {
        while( low < high && A[ high ] >= pivotkey )
            high--;
        swap(A, low, high);
        while( low < high && A[ low ] <= pivotkey)="" low++;="" swap(a,="" low,="" high);="" }="" return low;
}


排序算法的内容就差不多先写到这里了,其实学习算法,光能用代码写出算法和理解原理只是初步的学习,最近看了《算法导论》才认识到,一个算法不光光只像这两篇文章里写的内容那么简单,还包含各自复杂的数学证明和进一步的优化,我的学习还有待继续深入!

舞动的排序算法

如果光看文字还理解不了算法原理的话,这里有一组用舞蹈的形式来展现排序算法的原理的视频。

排序算法(一)

回顾基础的排序算法

在数据结构这门课程里,学校的老师们是较侧重于教学生如何利用诸如C语言来实现线性表,队列,栈,树这些简单的数据结构,理解这些数据结构的用法,而往往轻视了后半块关于图论,排序,查找等偏算法分析方面的内容的教学。

虽然课程的名字叫做数据结构,但很多CS强校和国外的著作都习惯地把它称作“数据结构与算法分析”。说到底,数据结构只能算是对编程语言使用的一个延伸,很多程序设计入门书诸如《C Primer Plus》和《C和指针》的后半段都会谈一些链表,树等内容,所以现在回过头来看,前面数据结构的学习还是为了在后面实现一些复杂的算法的时候,能够更好地利用这些结构来处理数据和实现算法中的思想。

在期末考试中,我们最后的大题目往往都是解答题,要求学生用编程语言实现诸如快速排序这样的算法,那么这样来看的话,独立实现这些复杂的算法,便就是真正考验这门课程是否学好的重要标准之一。

这一篇就先回顾排序算法


冒泡排序(Bubble Sort)

冒泡排序是一种比较简单的排序算法,通常被用来对于刚入门学习程序设计的学生介绍算法的概念。

算法原理:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

算法分析:

  • 若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数(n-1)和记录移动次数(0)均达到最小值。
    所以,冒泡排序最好的时间复杂度为O(n)。

  • 若初始文件是反序的,需要进行趟排序。每趟排序要进行次关键字的比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:O(n²)

  • 冒泡排序的最坏时间复杂度为O(n²)。

综上,因此冒泡排序总的平均时间复杂度为O(n²)。

算法稳定性:

由于冒泡算法的核心思想就是把小的元素往前调,大的元素往后调,相同的元素不做处理,所以即使相同的元素中间还隔着其他元素,这两个元素也不会发生交换,所以冒泡排序是一种稳定的排序算法。

算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef int ElemType;

void BubbleSort( ElemType A[], int N )
{
int i, j;
ElemType Tmp;
for( i = 0; i < N - 1; i++)
for( j = 0; j < N - 1 - i; j++)
{
if( A[ j ] > A[ j + 1] )
{
Tmp = A[ j ];
A[ j ] = A[ j + 1];
A[ j + 1] = Tmp;
}
}
}

插入排序(Insertion Sort)

插入排序由N-1趟排序组成,并且利用了这样的事实:位置0到位置P-1上的元素是排过序的。

它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

算法原理:

  1. 从第一个元素开始,该元素可以认为已经被排序。
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描。
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置。
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置。
  5. 将新元素插入到该位置后
  6. 重复步骤2~5

算法分析:

  • 由于嵌套循环的每一个都花费N次迭代,因此插入排序为O(n²),而且这个界是精确的,因为以反序输入同样可以达到该界。
  • 如果输入数据是有序的,那么运行时间为O(n),因为内层的for循环的检测总是立即判定不成立而终止。

综上,插入排序的平均情形也是O(n²)。

算法稳定性:

插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
typedef int ElemType;

void InsertionSort( ElemType A[], int N )
{
int j, P;

ElemType Tmp;
for( P = 1; P < N; P++ )
{
Tmp = A[ P ];
for( j = P; j > 0 && A[ j - 1 ] > Tmp; j-- )
A[ j ] = A[ j - 1 ];
A[ j ] = Tmp;
}
}
```


---

# 希尔排序:(Shell Sort)
希尔排序是插入排序的一种更高效的改进版本,通过比较相距一定距离的元素来工作,各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止,所以希尔排序有时也叫缩小增量排序。

增量是希尔排序的重要部分,只要最终增量为1任何增量序列(*increment sequence*)都可以工作。算法最开始以一定的增量进行排序。然后会继续以一定增量进行排序,最终算法以增量为1进行排序。当增量为1时,算法变为插入排序,这就保证了数据一定会被排序。

## 算法原理:
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量`d2<d1`重复上述的分组和排序,直至所取的增量`dt=1(dt<dt-l<…<d2<d1)`,即所有记录放在同一组中进行直接插入排序为止。

增量的选择有很多,并且关乎算法的效率。增量序列的一种流行选择是使用Shell建议的序列,`d1 = [ N / 2]`和`d(t) = [ d(t-1)/2 ]`。

已知的最好步长串行是由Sedgewick提出的 `(1, 5, 19, 41, 109,...)`,该串行的项来自 `9 * 4^i - 9 * 2^i + 1` 和 `4^i - 3 * 2^i + 1` 这两个算式,这项研究也表明“比较在希尔排序中是最主要的操作,而不是交换。”用这样步长串行的希尔排序比插入排序和堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。

## 算法分析:
#### 优劣

希尔排序是基于插入排序的一种算法,在此算法基础之上增加了一个新的特性,提高了效率。希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n²),而Hibbard增量的希尔排序的时间复杂度为O(n^3/2),但是现今仍然没有人能找出希尔排序的精确下界。希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O(n²)复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。

#### 时间性能

1. 增量序列的选择
Shell排序的执行时间依赖于增量序列。
好的增量序列的共同特征:
- 最后一个增量必须为1
- 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。
有人通过大量的实验,给出了较好的结果:当n较大时,比较和移动的次数约在nl.251.6n1.25之间。

2. Shell排序的时间性能优于直接插入排序
希尔排序的时间性能优于直接插入排序的原因:
①当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。
②当n值较小时,n和的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0()差别不大。
③在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。

因此,希尔排序在效率上较直接插入排序有较大的改进。

## 算法稳定性:
由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。

## 算法实现:
```C
typedef int ElemType;

void Shellsort( ElemType A[], int N )
{
int i, j, Increment;
ElemType Tmp;

for( Increment = N / 2; Increment > 0; Increment /= 2 )
for( i = Increment; i < N; i++ )
{
Tmp = A[ i ];
for( j = i; j >= Increment; j -= Increment )
if( Tmp < A[ j - Increment ] )
A[ j ] = A[ j - Increment];
else
break;
A[ j ] = Tmp;
}
}

选择排序(Selection sort)

选择排序(Selection sort)是一种简单直观的排序算法。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

算法原理:

  1. 从未排序的数组中选择最小的元素,组成有序序列。
  2. 从无序序列中继续挑选最小的元素,插入有序序列。
  3. 以此类推,直到全部转化为有序序列。

算法分析:

选择排序的交换操作介于0和(n-1)次之间。选择排序的比较操作为n(n-1)/2次之间。选择排序的赋值操作介于0和3(n-1)次之间。

比较次数O(n^2),比较次数与关键字的初始状态无关,总的比较次数N=(n-1)+(n-2)+...+1=n*(n-1)/2。 交换次数O(n),最好情况是,已经有序,交换0次;最坏情况是,逆序,交换n-1次。 交换次数比冒泡排序少多了,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。

算法稳定性:

选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。

算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef int ElemType;

void Selectsort( ElemType A[], int N )
{
int i, j, loc;
ElemType min, Tmp;

for( i = 0; i < N; i++)
{
min = A[ i ];
for( j = i; j < N; j++)
{
if( A[ j ] <= min )
{
min = A[ j ];
loc = j;
}
}
Tmp = A[ i ];
A[ i ] = A[ loc ];
A[ loc ] = Tmp;
}
}

堆排序(Heapsort)

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

堆节点的访问

通常堆是通过一维数组来实现的。在起始数组为 0 的情形中:

  • 父节点i的左子节点在位置 (2*i+1);
  • 父节点i的右子节点在位置 (2*i+2);
  • 子节点i的父节点在位置 floor((i-1)/2);

堆的操作

在堆的数据结构中,堆中的最大值总是位于根节点。堆中定义以下几种操作:

  • 最大堆调整(Max_Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
  • 创建最大堆(Build_Max_Heap):将堆所有数据重新排序
  • 堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算

算法原理:

  1. 先将无序的数组建立成一个最大堆(即最大值位于根结点的完全二叉树)。
  2. 依次从最大堆中删除最大值,放入数组尾部,并将最大堆重新调整。直到全部删除为止。

算法分析:

堆排序的时间,主要由建立初始堆和反复重建堆这两部分的时间开销构成,它们均是通过调用PercDown(下滤算法)实现的。

平均性能 O(N*logN)。

其他性能

  • 由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。
  • 堆排序是就地排序,辅助空间为O(1).
  • 在最坏的情况下,堆排序最多使用2NlogN - O(N)次比较。

算法稳定性:

它是不稳定的排序方法。

算法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

#define LeftChild(i) (2*(i)+1)
typedef int ElemType;

void PercDown( ElemType A[], int i, int N )
{
int Child;
ElemType Tmp;

for( Tmp = A[ i ]; LeftChild( i ) < N; i = Child )
{
Child = LeftChild( i );
if( Child != N - 1 && A[ Child + 1 ] > A[ Child ])
Child++;
if( Tmp < A[ Child ])
A[ i ] = A[ Child ];
else
break;
}
A[ i ] = Tmp;
}

void Heapsort( ElemType A[], int N )
{
int i;
ElemType Tmp;
for( i = N / 2; i >= 0; i-- )
PercDown( A, i, N);
for( i = N - 1; i > 0; i-- )
{
Tmp = A[ 0 ];
A[ 0 ] = A[ i ];
A[ i ] = Tmp;
PercDown( A, 0, i );
}
}