字符串匹配算法(四)——Boyer-Moore算法

1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授提出了另一种在O(n)时间复杂度内,完成字符串匹配的算法,其在绝大多数场合的性能表现,比KMP算法还要出色。

Boyer-Moore算法不仅高效,而且构思巧妙,文本编辑器的”查找”功能(Ctrl+F),大多采用的是该算法。

Boyer-Moore算法基本原理是从右向左扫描pattern;与KMP算法类似当遇到不匹配的字符时它也不需要对文本串text进行回溯,而是通过预先计算的Bad-character(坏字符)跳转表以及Good-suffix(好后缀)跳转表进行寻找最大的跳转长度。

下面以Moore教授的例子来讲解一下。这个例子的原始地址: http://www.cs.utexas.edu/~moore/best-ideas/string-searching/fstrpos-example.html

假定文本串text为:”HERE IS A SIMPLE EXAMPLE”,长度为n;模式串pattern为:”EXAMPLE”,长度为m;现在要在text中搜索看看是否包含pattern。

1).

HERE IS A SIMPLE EXAMPLE
EXAMPLE
首先text与pattern左边对齐,从右边开始进行比较。这个方法比较巧妙,因为如果最右边的字符不匹配,那么就只要比较一次就可以知道前7个字符肯定是不相同的。 S与E不匹配,这时'S'被称为Bad-character。并且'S'不包含在pattern之中,这样就可以把pattern直接移动到text的'S'的后一位。 2).
HERE IS A SIMPLE EXAMPLE
       EXAMPLE
仍然是从右边开始比较,发现'P'与'E'不匹配,因此'P'是Bad-character。但是'P'包含中pattern中,因此将pattern后移2位,把两个'P'对齐。注意这里后移时需要用到Bad-character跳转表,这个在后面再介绍。 3).
HERE IS A SIMPLE EXAMPLE
         EXAMPLE

再从右边开始比较,’E’与’E’相匹配,然后再比较下一个字符。

4).

HERE IS A SIMPLE EXAMPLE
         EXAMPLE
几次比较之后得到这样的结果,'A'与'I'不匹配,因此'I'是Bad-character。但是'MPLE'与'MPLE'是匹配的。我们尾部匹配的字符串称为Good-suffix(好后缀)。 5). 根据Bad-character的跳转方法,可以将pattern后移3位,变成如下形式。
HERE IS A SIMPLE EXAMPLE
            EXAMPLE

但是我们可以有更好的方法。这里’MPLE’为Good-suffix,根据Good-suffix跳转方法,将pattern后移6位,变成如下形式。同样的Good-suffix跳转表也将在后面介绍。

HERE IS A SIMPLE EXAMPLE
               EXAMPLE

6).
‘P’与’E’不匹配,因此’P’为Bad-character。再将pattern后移2位,最后变成:

HERE IS A SIMPLE EXAMPLE
                 EXAMPLE
至此整个字符串的匹配就完成了。
HERE IS A SIMPLE EXAMPLE
                 EXAMPLE

接下来介绍一下Bad-character和Good-suffix的跳转方法。
(1).对于Bad-character后移位数计算公式为:
delta1(x) = m ;x != pattern[j] (1 <= j <= m),即坏字符x中pattern中未出现
delta1(x) = m - max{k|pattern[k]=x, 1 <=k<=m} ;其他情况

例如上面的第2步时,’P’与’E’不匹配,因此’P’为Bad-character,在pattern中上次出现的位置为5,因此后移位数就是 7 - 5 = 2。

(2).对于Good-suffix后移位数计算方法为:
1.pattern中间的某一子字符串pattern[j-s+1:m-s] == pattern[j+1:m],可将pattern右移s位;
2.pattern已比较部分pattern[j+1:m]的后缀pattern[s+1:m]与pattern的前缀pattern[1:m-s]相同,可将pattern右移s位。
满足上面情况的s的最小值为最佳右移距离。
delta2(j) = min{s|(pattern[j+1:m]=pattern[j-s+1:m-s])&&(pattern[j]!=pattern[j-s])(j>s),pattern[s+1:m]=pattern1:m-s}

例如上面的第5步,满足的是情况2.pattern[6+1:7] == pattern[1:7-6] => pattern[7] == pattern[1],即是’E’ == ‘E’,所以s为6。故右移6位。

在匹配过程中,取delta1和delta2中较大的那一个。

用C语言实现如下:

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define ALPHABET_LEN 256
#define NOT_FOUND patlen
#define max(a, b) ((a < b) ? b : a)

// delta1 table: delta1[c] contains the distance between the last
// character of pat and the rightmost occurence of c in pat.
// If c does not occur in pat, then delta1[c] = patlen.
// If c is at string[i] and c != pat[patlen-1], we can
// safely shift i over by delta1[c], which is the minimum distance
// needed to shift pat forward to get string[i] lined up
// with some character in pat.
// this algorithm runs in alphabet_len+patlen time.
void make_delta1(int *delta1, uint8_t *pat, int32_t patlen) {
int i;
for (i=0; i < ALPHABET_LEN; i++) {
delta1[i] = NOT_FOUND;
}
for (i=0; i < patlen-1; i++) {
delta1[pat[i]] = patlen-1 - i;
}
}

// true if the suffix of word starting from word[pos] is a prefix
// of word
int is_prefix(uint8_t *word, int wordlen, int pos) {
int i;
int suffixlen = wordlen - pos;
// could also use the strncmp() library function here
for (i = 0; i < suffixlen; i++) {
if (word[i] != word[pos+i]) {
return 0;
}
}
return 1;
}

// length of the longest suffix of word ending on word[pos].
// suffix_length("dddbcabc", 8, 4) = 2
int suffix_length(uint8_t *word, int wordlen, int pos) {
int i;
// increment suffix length i to the first mismatch or beginning
// of the word
for (i = 0; (word[pos-i] == word[wordlen-1-i]) && (i < pos); i++);
return i;
}

// delta2 table: given a mismatch at pat[pos], we want to align
// with the next possible full match could be based on what we
// know about pat[pos+1] to pat[patlen-1].
//
// In case 1:
// pat[pos+1] to pat[patlen-1] does not occur elsewhere in pat,
// the next plausible match starts at or after the mismatch.
// If, within the substring pat[pos+1 .. patlen-1], lies a prefix
// of pat, the next plausible match is here (if there are multiple
// prefixes in the substring, pick the longest). Otherwise, the
// next plausible match starts past the character aligned with
// pat[patlen-1].
//
// In case 2:
// pat[pos+1] to pat[patlen-1] does occur elsewhere in pat. The
// mismatch tells us that we are not looking at the end of a match.
// We may, however, be looking at the middle of a match.
//
// The first loop, which takes care of case 1, is analogous to
// the KMP table, adapted for a 'backwards' scan order with the
// additional restriction that the substrings it considers as
// potential prefixes are all suffixes. In the worst case scenario
// pat consists of the same letter repeated, so every suffix is
// a prefix. This loop alone is not sufficient, however:
// Suppose that pat is "ABYXCDEYX", and text is ".....ABYXCDEYX".
// We will match X, Y, and find B != E. There is no prefix of pat
// in the suffix "YX", so the first loop tells us to skip forward
// by 9 characters.
// Although superficially similar to the KMP table, the KMP table
// relies on information about the beginning of the partial match
// that the BM algorithm does not have.
//
// The second loop addresses case 2. Since suffix_length may not be
// unique, we want to take the minimum value, which will tell us
// how far away the closest potential match is.
void make_delta2(int *delta2, uint8_t *pat, int32_t patlen) {
int p;
int last_prefix_index = patlen-1;

// first loop
for (p=patlen-1; p>=0; p--) {
if (is_prefix(pat, patlen, p+1)) {
last_prefix_index = p+1;
}
delta2[p] = last_prefix_index + (patlen-1 - p);
}

// second loop
for (p=0; p < patlen-1; p++) {
int slen = suffix_length(pat, patlen, p);
if (pat[p - slen] != pat[patlen-1 - slen]) {
delta2[patlen-1 - slen] = patlen-1 - p + slen;
}
}
}

uint8_t* boyer_moore (uint8_t *string, uint32_t stringlen, uint8_t *pat, uint32_t patlen) {
int i;
int delta1[ALPHABET_LEN];
int *delta2 = malloc(patlen * sizeof(int));
make_delta1(delta1, pat, patlen);
make_delta2(delta2, pat, patlen);

i = patlen-1;
while (i < stringlen) {
int j = patlen-1;
while (j >= 0 && (string[i] == pat[j])) {
--i;
--j;
}
if (j < 0) {
free(delta2);
printf("index: %d\n", i);
return (string + i+1);
}

i += max(delta1[string[i]], delta2[j]);
}
free(delta2);
return NULL;
}

int main(int argc, char *argv[])
{

char text[] = "HERE IS A SILMPLE EXAMPLE";
char pattern[] = "EXAMPLE";
char *result = boyer_moore(text, strlen(text), pattern, strlen(pattern));
if (result)
{
printf("result: %s\n", result);
} else {
printf("failed!\n");
}
return 0;
}

字符串匹配算法(三)——KMP算法

KMP算法是常用的字符串匹配算法之一,是由Knuth、Pratt和Morris发明的。其中Knuth就是著名科学家Donald Knuth。

该算法可以在O(n+m)的时间数量级上完成模式匹配操作。与传统算法相比其改进在于每当一趟匹配过程中出现比较不相等时不用回溯目标字符串。

这个算法还是比较难理解,下面通过一个例子来讲解。
有一个目标字符串”BBC ABCDAB ABCDABCDABDE”,我想知道,里面是否包含另一个字符串”ABCDABD”?

1).

BBC ABCDAB ABCDABCDABDE
ABCDABD
首先目标字符串”BBC ABCDAB ABCDABCDABDE”的第一个字符与源字符串”ABCDABD”的第一个字符相比,不匹配,则目标字符串后移一位。 2).
BBC ABCDAB ABCDABCDABDE
 ABCDABD
B与A不匹配,再次后移。 3).
BBC ABCDAB ABCDABCDABDE
    ABCDABD
就这样直到有一个字符相同为止。 4).
BBC ABCDAB ABCDABCDABDE
    ABCDABD
接着比较下一个字符,还是相同的。再继续。 5).
BBC ABCDAB ABCDABCDABDE
    ABCDABD
这时又出现了字符不匹配的情况,如果是传统的算法的话就把目标字符串进行回溯,源字符串后移一位。变成如下情形:
BBC ABCDAB ABCDABCDABDE
     ABCDABD
毕竟这也是最容易理解的,但是这样一来很多字符又需要再重新比较一次。因此效率比较低。 6). 虽然D与空格不匹配,但是前面的ABCDAB是相匹配的。KMP算法就是利用这个已知信息,不要把“搜索”位置称回已经比较过的位置,而且继续比较之后的,这样就减少了重复的比较,提高了效率。继续向后移动变成如下情形:
BBC ABCDAB ABCDABCDABDE
        ABCDABD

这个移动过程需要一张部分匹配表:

j        1234567
源字符串 ABCDABD
next[j]  0111123
因为前6个字符是匹配的,最后一个匹配的字符B对应next值为2,根据公式: 移动位数=已匹配字符数-对应的next值 因此将源字符串后移6-2=4位。 7).因为空格与C不相同,继续后移。
BBC ABCDAB ABCDABCDABDE
          ABCDABD

8).空格与A不相同,继续重复之前的操作进行后移。

BBC ABCDAB ABCDABCDABDE
           ABCDABD

9).这里C与D不相同,继续用之前的方法进行后移。

BBC ABCDAB ABCDABCDABDE
               ABCDABD
最后匹配完成,字符串”BBC ABCDAB ABCDABCDABDE”里面包含字符串”ABCDABD”。 下面介绍一下部分匹配表的计算方法。 公式为: next[j] = 0 (j=1) next[j] = Max {k 1<k<j 且 'p(1)...p(k-1)' = 'p(j-k+1)...p(j-1)'} (当此集合不为空时) next[j] = 1 (其他情况) 然后根据公式讲解一下上面例子中的字符串的部分匹配表的计算。(注:这里讲解时是按照数组下标从1开始的,而在C语言中是从0开始的)
j        1234567
源字符串 ABCDABD

由公式得next[1] = 0, next[2] = 1, next[3] = 1, next[4] = 1, next[5] = 1。
当j=6时,p[1]=p5 => k=2 => next[6] = 2。
当j=7时,p[1:2]=p5:6 => k=3 => next[7] = 3。

用C语言实现如下:

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
#include <stdio.h>

// KMP算法的重点就是求next函数值
void make_next(const char *pattern, int len, unsigned int *next)
{
int i=1, j=0;
next[1] = 0;

while (i <= len) {
if (j == 0 || pattern[i-1] == pattern[j-1]) {
i++;
j++;
next[i] = j;
} else {
j = next[j];
}
}
}

int kmp_match(char *t, char *p, int next[], int len1, int len2)
{
int i=0, j=0;
while (i < len1 && j < len2) {
if (j == 0 || t[i] == p[j]) {
i++;
j++;
} else {
j = next[j];
}
}
if (j >= len2 ) return i - len2;
return -1;
}

int main(int argc, char *argv[])
{
char t[] = "BBC ABCDAB ABCDABCDABDE";
char p[] = "ABCDABD";
printf("t: %s\np: %s\n", t, p);
int len = sizeof(p);
int next[len];
make_next(p, len-1, next);
int i;
for (i=1; i<len; i++) {
printf("%4d", next[i]);
}
printf("\n");
printf("index: %d\n", kmp_match(t, p, next, sizeof(t)-1, len-1));
return 0;
}

Python中的排序算法——Timsort

突然对Python中list对象的sort函数比较好奇,想知道它用的是什么算法。正好最近我也在复习算法,于是就随便把Python的sort也研究一下吧。刚开始我猜想可能用是快速排序之类的,最后看了 Python的源代码之后发现其实用的是一个适应性强的、稳定的、自然的归并算法,名为timsort。

Timsort是一种混合的算法,它派生自归并排序和插入排序。它是2002年由Tim Peters为Python语言发明的。
Timsort的时间复杂度最好情况为O(n),平均情况为O(nlogn),最差情况为O(nlogn);空间复杂度为O(n)。
更多关于Timsort的信息: http://en.wikipedia.org/wiki/Timsort

《Python Cookbook(第2版)中文版》这本书的第5章中由Tim Peters介绍了Python排序算法发展简史。其中说到Timsort算法的实现包括了大约1200行C程序,而且大概有一半是在实现一个技术上的技巧。可见该算法还是很复杂的。

在Python源代码目录下有Objects/listsort.txt这个文件,这是Tim Peters写的一个详细的文档。

有个项目致力于用纯C来实现Timsort算法,如Tim所言代码量超过了1200行。http://code.google.com/p/timsort/

字符串匹配算法(二)——Rabin-Karp算法

Rabin-Karp设计了一个巧妙的hash算法。用到的数学公式为:t(s+1) = (d(t(s)-T[s+1]h)+T[s+m+1]) mod q
t(s+1)为目标字符串的子字符串T[s+1,s+m+1]的哈希值,因为用到了上一次的结果t(s),所以可以节省计算时间。

首先计算pattern字符串的hash值,然后在从目标字符串的开头,计算相同长度字符串的hash值。若hash值相同,则表示匹配,若不同,则向右移动一位,计算新的hash值。整个过程,与暴力的字符串匹配算法很相似,但由于计算hash值时,可以利用上一次的hash值,从而使新的hash值只需要加上新字母的计算,并减去上一次的第一个字母的计算,即可。

Rabin-Karp算法的预处理时间为O(m),最坏情况下该算法的匹配时间为O((n-m+1)m)。但是平均运行时间还是比较好的。

用C语言实现如下:

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
#include <stdio.h>
#include <math.h>

int mod = 0x7fffffff;
const int d = 128;

int rabin_karp(char *T, char *P, int n, int m)
{
if (n < m) return -2;
int h = pow(d, m-1);
int p = 0;
int t = 0;
int i, j;
for (i=0; i<m; i++) {
p = (d*p + P[i]) % mod;
t = (d*t + T[i]) % mod;
}
for (j=0; j<=n-m; j++) {
if (p == t) {
return j;
}
if (j < n-m) {
t = (d*(t - h*T[j]) + T[j+m]) % mod;
}
}
return -1;
}

int main(int argc, char *argv[])
{
char t[] = "BBC ABCDAB ABCDABCDABDE";
char p[] = "ABCDABD";
int len1 = sizeof(t) - 1;
int len2 = sizeof(p) - 1;
int index = rabin_karp(t, p, len1, len2);
printf("index: %d\n", index);
return 0;
return 0;
}

现在结合上面的公式和代码再进行讲解一下。
d表示字母表的字母个数,上面的代码只接受ascii值为0~127的字符。如果采用小写英文字母来做字母表的话,那么d就是26。
h等于d^(m-1) % q。其实模q运算应该是可有可无的,加入q应该是为了避免数据溢出。但是加入模q后,由ts == p mod q不能说明ts == p,不过ts != p mod q则可以肯定ts != p。所以q最好选择比较大的质数,并且选取的q要满足使dq的值在一个计算机字长内。如果使用的是动态的编程语言的话就不用担心数据溢出,因此就不用进行模q运算。

字符串匹配算法(一)——朴素算法

写python代码写习惯了,再来用C代码处理字符串感觉很痛苦。在python中有find、index等函数可以进行字符串匹配,而用C语言就只能自己写了。

朴素算法思路比较简单,就是将源字符串与目标字符串挨个进行比较,看看在目标字符串中是否包含了源字符串。该算法的匹配时间为O((n-m+1)m)。

C语言实现如下:

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
#include <stdio.h>

int native_matcher(char *t, char *p, int len1, int len2)
{
int i, j, idx;
idx = 0;
for (i=0; i<len1; i++) {
for (j=0; j<len2; j++) {
if (p[j] != t[j+i]) {
break;
}
}
if (j == len2) {
return i;
}
}
return -1;
}

int main(int argc, char *argv[])
{
char t[] = "BBC ABCDAB ABCDABCDABDE";
char p[] = "ABCDABD";
int len1 = sizeof(t) - 1;
int len2 = sizeof(p) - 1;
int index = native_matcher(t, p, len1, len2);
printf("index: %d\n", index);
return 0;
}

常用的排序算法(八)——桶排序

桶排序算法对待排数列有一定的要求。数据长度必须一样,并且符合均匀分布。

桶排序的基本思想就是先创建一个含n个元素的数组,即就是创建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
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
const int N = 20;
const int M = 10;

struct chain
{
int key;
struct chain *next;
};

void init_bucket(struct chain a[], int n)
{
int i = 0;
for(; i < n; i++)
{
a[i].key = i * 10;
a[i].next = NULL;
}
}

int * generate(int a[], int n)
{
int i;
for(i = 0; i < n; i++)
{
a[i] = 0;
}
time_t t;
srand((unsigned)time(&t));

for(i = 0; i < n; i++)
{
a[i] = rand() % 100;
}
return a;
}


void print_array(int *a, int n)
{
int i = 0;
while(i < n)
{
printf("%4d", a[i]);
i++;
}
printf("\n");
}

void print_bucket(struct chain a[], int n)
{
int i = 0;
for(; i < n; i++)
{
printf("%4d", a[i].key);
}
printf("\n");
}


void insertChain(struct chain node, struct chain *newNode)
{
if(node.next == NULL)
{
node.next = newNode;
}
else
{
struct chain *p = node.next;
struct chain *q = p;
while(p->key < newNode->key)
{

q = p;
p = p->next;
}
newNode->next = q->next;
q->next = newNode;

}
}

void print_keys(struct chain a[], int n)
{
int i = 0;
for(; i < n; i++)
{
if(a[i].next != NULL)
{
struct chain *p = a[i].next;
while(p->next != NULL)
{
printf("%4d", p->key);
p = p->next;
}
printf("%4d", p->key);
}
}
}

void insert_bucket(struct chain a[], int *keys, int n)
{
int i = 0;
for(; i < n; i++)
{
struct chain *newNode = (struct chain *)malloc(sizeof(struct chain));
newNode->key = keys[i];
newNode->next = NULL;

struct chain *node = &a[keys[i] / 10];

if(node->next == NULL)
{
node->next = newNode;
}
else
{
struct chain *p = node->next;
struct chain *q = p;
while((p->key <= newNode->key) && (p->next != NULL))
{

q = p;
p = p->next;
}
newNode->next = q->next;
q->next = newNode;
}
}
}

int main()
{
struct chain heads[M];
init_bucket(heads, M);
printf("bucket: ");
print_bucket(heads, M);

int keys[N];
generate(keys,N);
printf("numbers:");
print_array(keys, N);

insert_bucket(heads, keys, N);
printf("ordered:");
print_keys(heads, M);
printf("\n");
return 0;
}

常用的排序算法(七)——希尔排序

希尔排序又称缩小增量排序。它是插入排序的一种改进版。

它的基本思想是:先将整个待排序列分割成若干子序列分别进行直接插入排序,待整个序列基本有序时再进行一次直接插入排序。

例如对于数列A={49, 38, 65, 97, 76, 13, 27, 49, 55, 04},首先将数列分成五个子序列{A0, A5}, {A1, A6}, {A2,A7}, {A3,A8}, {A4,A9},分别对它们进行直接插入排序。于是第1趟希尔排序之后数列为:{ 13 27 49 55 4 49 38 65 97 76 }。然后再将该数列分成两个子数列{A0,A2,A4,A6,A8}, {A1,A3,A5,A7,A9},于是第3趟希尔排序之后数列为:{ 4 27 13 49 38 55 49 65 97 76 }。最后再进行一次插入排序即可。

用C语言实现如下:

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
#include <stdio.h>

void print_list(int *ar, int n)
{
int i;
printf("[ ");
for (i=0; i<n; i++) {
printf("%d ", ar[i]);
}
printf("]\n");

}

void shell_sort(int *ar, int n)
{
int d, i, j, tmp;
d = n / 2;
while (d >= 1) {
for (i=d; i<n; i++) {
tmp = ar[i];
j = i - d;
while (j >= 0 && tmp < ar[j]) {
ar[j+d] = ar[j];
j -= d;
}
ar[j+d] = tmp;
}
d = d / 2;
print_list(ar, n);
}
}

int main(int argc, char *argv[])
{
int a[] = {49, 38, 65, 97, 76, 13, 27, 49, 55, 04};
int len=sizeof(a)/sizeof(int);
printf("begin: ");
print_list(a, len);
shell_sort(a, len);
printf("end ");
print_list(a, len);
return 0;
}

希尔排序演示图片:

常用的排序算法(六)——快速排序

快速排序也是分治算法的一种应用。它的基本操作是根据某种方法(可以是随机的,也可以是最左边,或者是中间的)选择一个元素作为中枢值。这个元素将数组分为两个子数组,小于或等于中枢值的元素都移到左边,大于或等于中枢值的元素都移到右边。然后每个子数组再递归地排序。

快速排序的平均时间复杂度为O(nlogn),最好时为O(nlogn),最坏时为O(n*n)。

用C语言实现如下:

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
#include <stdio.h>

void print_list(int *ar, int n)
{
int i;
printf("[ ");
for (i=0; i<n; i++) {
printf("%d ", ar[i]);
}
printf("]\n");

}

int partition(int *ar, int left, int right)
{
int x, i, j;
int tmp;

x = ar[right];
i = left;
for (j=left; j<right; j++) {
if (ar[j] <= x) {
tmp = ar[j];
ar[j] = ar[i];
ar[i] = tmp;
i++;
}
}
tmp = ar[i];
ar[i] = ar[right];
ar[right] = tmp;
return i;
}

void quick_sort(int *ar, int left, int right)
{
int index;
if (left < right) {
index = partition(ar, left, right);
quick_sort(ar, left, index-1);
quick_sort(ar, index, right);
}
}

int main(int argc, char *argv[])
{
int a[] = {15, 9, 8, 1, 4, 11, 7, 12, 13, 16, 5, 3, 16, 2, 10, 14};
int len=sizeof(a)/sizeof(int);
printf("begin: ");
print_list(a, len);
quick_sort(a, 0, len-1);
printf("end ");
print_list(a, len);
return 0;
}

这个例子是选择最右边的元素作为中枢值。

快速排序演示图片:

常用的排序算法(五)——堆排序

堆排序的时间复杂度为O(nlogn)。

首先简单介绍一下堆,一个堆其实就是一个近似的完全二叉树,它具有以下两个性质:
外形性质:如果深度k-1存在2**(k-1)个节点,那么深度k(k>0)上存在叶子节点。另外,在一个部分填充的深度级上,节点是“ 从左到右”添加的。

堆的性质:树中每一个节点的值都大于或者等于任意一个子节点的值。

堆只是一个抽象的数据结构,在内存可以使用数组来表示的。第0个值为根节点。对于第i个节点,其左子节点为2i+1,右子节点为2i+2。

例如,对于数列[ 5 3 16 2 10 14 ],将其构造一个堆则为:

1
2
3
4
5
    16
/ \
10 14
/ \ /
2 3 5

然后这个堆对应的数组为[ 16 10 14 2 3 5 ],最后就是对这个堆进行排序。

代码如下:

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
#include <stdio.h>

void print_list(int *ar, int n)
{
int i;
printf("[ ");
for (i=0; i<n; i++) {
printf("%d ", ar[i]);
}
printf("]\n");

}
static void heapify(int *ar, int idx, int max)
{
int left = 2*idx + 1;
int right = 2*idx +2;
int largest;

// 计算A[idx], A[left], A[right]中最大的一个
if (left < max && ar[left] > ar[idx]) {
largest = left;
} else {
largest = idx;
}

if (right < max && ar[right] > ar[largest]) {
largest = right;
}

// 如果最大的不是父节点,那么交换并递归执行
if (largest != idx) {
int tmp;
tmp = ar[idx];
ar[idx] = ar[largest];
ar[largest] = tmp;

heapify(ar, largest, max);
}
}

static void buildHeap(int *ar, int n)
{
int i;
for (i=n/2-1; i>=0; i--) {
heapify(ar, i, n);
}
}

void heap_sort(int *ar, int n)
{
int i;
int tmp;
buildHeap(ar, n);
printf("build:");
for (i=n-1; i>=1; i--) {
tmp = ar[0];
ar[0] = ar[i];
ar[i] = tmp;

heapify(ar, 0, i);
}
}

int main(int argc, char *argv[])
{
int a[] = {5, 3, 16, 2, 10, 14};
int len=sizeof(a)/sizeof(int);
printf("begin: ");
print_list(a, len);
heap_sort(a, len);
printf("end ");
print_list(a, len);
return 0;
}

堆排序演示图片:

常用的排序算法(四)——选择排序

选择排序的时间复杂度为O(n*n)。

其基本操作是每一次从待排序的数列中先出最大(或最小)的一个元素,然后把它顺序的放在已排好序的数列的最后。

例如:
一个数列初始状态为:[ 5 3 16 2 10 14 ]
第一次排序后为: [ 5 3 14 2 10 ] 16
第二次排序后为: [ 5 3 10 2 ] 14 16
第三次排序后为: [ 5 3 2 ] 10 14 16
第四次排序后为: [ 2 3 ] 5 10 14 16
第五次排序后为: [ 2 ] 3 5 10 14 16
排序完成之后为: [ 2 3 5 10 14 16 ]

代码如下:

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
#include <stdio.h>

void print_list(int *ar, int n)
{
int i;
printf("[ ");
for (i=0; i<n; i++) {
printf("%d ", ar[i]);
}
printf("]\n");

}

int select_max(int *ar, int left, int right)
{
int max = left;
int i = left;
while (++i <= right) {
if (ar[i] > ar[max]) {
max = i;
}
}
return max;
}

void selection_sort(int *ar, int n)
{
int i, max, tmp;
for (i=n-1; i>=1; i--) {
max = select_max(ar, 0, i);
if (max != i) {
tmp = ar[i];
ar[i] = ar[max];
ar[max] = tmp;
}
}

}

int main(int argc, char *argv[])
{
int a[] = {5, 3, 16, 2, 10, 14};
int len=sizeof(a)/sizeof(int);
printf("begin: ");
print_list(a, len);
selection_sort(a, len);
printf("end ");
print_list(a, len);
return 0;
}

选择排序演示图片: