各种排序算法的比较
1.稳定性比较
插入排序、冒泡排序、二叉树排序、二路归并排序及其他线形排序是稳定的
选择排序、希尔排序、快速排序、堆排序是不稳定的
2.时间复杂性比较
插入排序、冒泡排序、选择排序的时间复杂性为O(n2)
其它非线形排序的时间复杂性为O(nlog2n)
线形排序的时间复杂性为O(n);
3.辅助空间的比较
线形排序、二路归并排序的辅助空间为O(n),其它排序的辅助空间为O(1);
4.其它比较
插入、冒泡排序的速度较慢,但参加排序的序列局部或整体有序时,这种排序能达到较快的速度。
反而在这种情况下,快速排序反而慢了。
当n较小时,对稳定性不作要求时宜用选择排序,对稳定性有要求时宜用插入或冒泡排序。
若待排序的记录的关键字在一个明显有限范围内时,且空间允许是用桶排序。
当n较大时,关键字元素比较随机,对稳定性没要求宜用快速排序。
当n较大时,关键字元素可能出现本身是有序的,对稳定性有要求时,空间允许的情况下。
宜用归并排序。
当n较大时,关键字元素可能出现本身是有序的,对稳定性没有要求时宜用堆排序。
*************************************************************************************
重温经典排序思想--C语言常用排序全解
/*
=============================================================================
相关知识介绍(所有定义只为帮助读者理解相关概念,并非严格定义):
1、稳定排序和非稳定排序
简单地说就是所有相等的数经过某种排序方法后,仍能保持它们在排序之前的相对次序,我们就
说这种排序方法是稳定的。反之,就是非稳定的。
比如:一组数排序前是a1,a2,a3,a4,a5,其中a2=a4,经过某种排序后为a1,a2,a4,a3,a5,
则我们说这种排序是稳定的,因为a2排序前在a4的前面,排序后它还是在a4的前面。假如变成a1,a4,
a2,a3,a5就不是稳定的了。
2、内排序和外排序
在排序过程中,所有需要排序的数都在内存,并在内存中调整它们的存储顺序,称为内排序;
在排序过程中,只有部分数被调入内存,并借助内存调整数在外存中的存放顺序排序方法称为外排序。
3、算法的时间复杂度和空间复杂度
所谓算法的时间复杂度,是指执行算法所需要的计算工作量。
一个算法的空间复杂度,一般是指执行这个算法所需要的内存空间。
001 | ================================================================================ |
002 | */ |
003 | /* |
004 | ================================================ |
005 | 功能:选择排序 |
006 | 输入:数组名称(也就是数组首地址)、数组中元素个数 |
007 | ================================================ |
008 | */ |
009 | /* |
010 | ==================================================== |
011 | 算法思想简单描述: |
012 | |
013 | 在要排序的一组数中,选出最小的一个数与第一个位置的数交换; |
014 | 然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环 |
015 | 到倒数第二个数和最后一个数比较为止。 |
016 | |
017 | 选择排序是不稳定的。算法复杂度O(n2)--[n的平方] |
018 | ===================================================== |
019 | */ |
020 | void select_sort( int *x, int n) |
021 | { |
022 | int i, j, min, t; |
023 | |
024 | for (i=0; i<n-1; i++) /*要选择的次数:0~n-2共n-1次*/ |
025 | { |
026 | min = i; /*假设当前下标为i的数最小,比较后再调整*/ |
027 | for (j=i+1; j<n; j++) /*循环找出最小的数的下标是哪个*/ |
028 | { |
029 | if (*(x+j) < *(x+min)) |
030 | { |
031 | min = j; /*如果后面的数比前面的小,则记下它的下标*/ |
032 | } |
033 | } |
034 | |
035 | if (min != i) /*如果min在循环中改变了,就需要交换数据*/ |
036 | { |
037 | t = *(x+i); |
038 | *(x+i) = *(x+min); |
039 | *(x+min) = t; |
040 | } |
041 | } |
042 | } |
043 | |
044 | |
045 | /* |
046 | ================================================ |
047 | 功能:直接插入排序 |
048 | 输入:数组名称(也就是数组首地址)、数组中元素个数 |
049 | ================================================ |
050 | */ |
051 | /* |
052 | ==================================================== |
053 | 算法思想简单描述: |
054 | |
055 | 在要排序的一组数中,假设前面(n-1) [n>=2] 个数已经是排 |
056 | 好顺序的,现在要把第n个数插到前面的有序数中,使得这n个数 |
057 | 也是排好顺序的。如此反复循环,直到全部排好顺序。 |
058 | |
059 | 直接插入排序是稳定的。算法时间复杂度O(n2)--[n的平方] |
060 | ===================================================== |
061 | */ |
062 | void insert_sort( int *x, int n) |
063 | { |
064 | int i, j, t; |
065 | |
066 | for (i=1; i<n; i++) /*要选择的次数:1~n-1共n-1次*/ |
067 | { |
068 | /* |
069 | 暂存下标为i的数。注意:下标从1开始,原因就是开始时 |
070 | 第一个数即下标为0的数,前面没有任何数,单单一个,认为 |
071 | 它是排好顺序的。 |
072 | */ |
073 | t=*(x+i); |
074 | for (j=i-1; j>=0 && t<*(x+j); j--) /*注意:j=i-1,j--,这里就是下标为i的数,在它前面有序列中找插入位置。*/ |
075 | { |
076 | *(x+j+1) = *(x+j); /*如果满足条件就往后挪。最坏的情况就是t比下标为0的数都小,它要放在最前面,j==-1,退出循环*/ |
077 | } |
078 | |
079 | *(x+j+1) = t; /*找到下标为i的数的放置位置*/ |
080 | } |
081 | } |
082 | |
083 | |
084 | /* |
085 | ================================================ |
086 | 功能:冒泡排序 |
087 | 输入:数组名称(也就是数组首地址)、数组中元素个数 |
088 | ================================================ |
089 | */ |
090 | /* |
091 | ==================================================== |
092 | 算法思想简单描述: |
093 | |
094 | 在要排序的一组数中,对当前还未排好序的范围内的全部数,自上 |
095 | 而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较 |
096 | 小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要 |
097 | 求相反时,就将它们互换。 |
098 | |
099 | 下面是一种改进的冒泡算法,它记录了每一遍扫描后最后下沉数的 |
100 | 位置k,这样可以减少外层循环扫描的次数。 |
101 | |
102 | 冒泡排序是稳定的。算法时间复杂度O(n2)--[n的平方] |
103 | ===================================================== |
104 | */ |
105 | |
106 | void bubble_sort( int *x, int n) |
107 | { |
108 | int j, k, h, t; |
109 | |
110 | for (h=n-1; h>0; h=k) /*循环到没有比较范围*/ |
111 | { |
112 | for (j=0, k=0; j<h; j++) /*每次预置k=0,循环扫描后更新k*/ |
113 | { |
114 | if (*(x+j) > *(x+j+1)) /*大的放在后面,小的放到前面*/ |
115 | { |
116 | t = *(x+j); |
117 | *(x+j) = *(x+j+1); |
118 | *(x+j+1) = t; /*完成交换*/ |
119 | k = j; /*保存最后下沉的位置。这样k后面的都是排序排好了的。*/ |
120 | } |
121 | } |
122 | } |
123 | } |
124 | |
125 | |
126 | /* |
127 | ================================================ |
128 | 功能:希尔排序 |
129 | 输入:数组名称(也就是数组首地址)、数组中元素个数 |
130 | ================================================ |
131 | */ |
132 | /* |
133 | ==================================================== |
134 | 算法思想简单描述: |
135 | |
136 | 在直接插入排序算法中,每次插入一个数,使有序序列只增加1个节点, |
137 | 并且对插入下一个数没有提供任何帮助。如果比较相隔较远距离(称为 |
138 | 增量)的数,使得数移动时能跨过多个元素,则进行一次比较就可能消除 |
139 | 多个元素交换。D.L.shell于1959年在以他名字命名的排序算法中实现 |
140 | 了这一思想。算法先将要排序的一组数按某个增量d分成若干组,每组中 |
141 | 记录的下标相差d.对每组中全部元素进行排序,然后再用一个较小的增量 |
142 | 对它进行,在每组中再进行排序。当增量减到1时,整个要排序的数被分成 |
143 | 一组,排序完成。 |
144 | |
145 | 下面的函数是一个希尔排序算法的一个实现,初次取序列的一半为增量, |
146 | 以后每次减半,直到增量为1。 |
147 | |
148 | 希尔排序是不稳定的。 |
149 | ===================================================== |
150 | */ |
151 | void shell_sort( int *x, int n) |
152 | { |
153 | int h, j, k, t; |
154 | |
155 | for (h=n/2; h>0; h=h/2) /*控制增量*/ |
156 | { |
157 | for (j=h; j<n; j++) /*这个实际上就是上面的直接插入排序*/ |
158 | { |
159 | t = *(x+j); |
160 | for (k=j-h; (k>=0 && t<*(x+k)); k-=h) |
161 | { |
162 | *(x+k+h) = *(x+k); |
163 | } |
164 | *(x+k+h) = t; |
165 | } |
166 | } |
167 | } |
168 | |
169 | |
170 | /* |
171 | ================================================ |
172 | 功能:快速排序 |
173 | 输入:数组名称(也就是数组首地址)、数组中起止元素的下标 |
174 | ================================================ |
175 | */ |
176 | /* |
177 | ==================================================== |
178 | 算法思想简单描述: |
179 | |
180 | 快速排序是对冒泡排序的一种本质改进。它的基本思想是通过一趟 |
181 | 扫描后,使得排序序列的长度能大幅度地减少。在冒泡排序中,一次 |
182 | 扫描只能确保最大数值的数移到正确位置,而待排序序列的长度可能只 |
183 | 减少1。快速排序通过一趟扫描,就能确保某个数(以它为基准点吧) |
184 | 的左边各数都比它小,右边各数都比它大。然后又用同样的方法处理 |
185 | 它左右两边的数,直到基准点的左右只有一个元素为止。它是由 |
186 | C.A.R.Hoare于1962年提出的。 |
187 | |
188 | 显然快速排序可以用递归实现,当然也可以用栈化解递归实现。下面的 |
189 | 函数是用递归实现的,有兴趣的朋友可以改成非递归的。 |
190 | |
191 | 快速排序是不稳定的。最理想情况算法时间复杂度O(nlog2n),最坏O(n2) |
192 | |
193 | ===================================================== |
194 | */ |
195 | void quick_sort( int *x, int low, int high) |
196 | { |
197 | int i, j, t; |
198 | |
199 | if (low < high) /*要排序的元素起止下标,保证小的放在左边,大的放在右边。这里以下标为low的元素为基准点*/ |
200 | { |
201 | i = low; |
202 | j = high; |
203 | t = *(x+low); /*暂存基准点的数*/ |
204 | |
205 | while (i<j) /*循环扫描*/ |
206 | { |
207 | while (i<j && *(x+j)>t) /*在右边的只要比基准点大仍放在右边*/ |
208 | { |
209 | j--; /*前移一个位置*/ |
210 | } |
211 | |
212 | if (i<j) |
213 | { |
214 | *(x+i) = *(x+j); /*上面的循环退出:即出现比基准点小的数,替换基准点的数*/ |
215 | i++; /*后移一个位置,并以此为基准点*/ |
216 | } |
217 | |
218 | while (i<j && *(x+i)<=t) /*在左边的只要小于等于基准点仍放在左边*/ |
219 | { |
220 | i++; /*后移一个位置*/ |
221 | } |
222 | |
223 | if (i<j) |
224 | { |
225 | *(x+j) = *(x+i); /*上面的循环退出:即出现比基准点大的数,放到右边*/ |
226 | j--; /*前移一个位置*/ |
227 | } |
228 | } |
229 | |
230 | *(x+i) = t; /*一遍扫描完后,放到适当位置*/ |
231 | quick_sort(x,low,i-1); /*对基准点左边的数再执行快速排序*/ |
232 | quick_sort(x,i+1,high); /*对基准点右边的数再执行快速排序*/ |
233 | } |
234 | } |
235 | |
236 | |
237 | /* |
238 | ================================================ |
239 | 功能:堆排序 |
240 | 输入:数组名称(也就是数组首地址)、数组中元素个数 |
241 | ================================================ |
242 | */ |
243 | /* |
244 | ==================================================== |
245 | 算法思想简单描述: |
246 | |
247 | 堆排序是一种树形选择排序,是对直接选择排序的有效改进。 |
248 | 堆的定义如下:具有n个元素的序列(h1,h2,...,hn),当且仅当 |
249 | 满足(hi>=h2i,hi>=2i+1)或(hi<=h2i,hi<=2i+1)(i=1,2,...,n/2) |
250 | 时称之为堆。在这里只讨论满足前者条件的堆。 |
251 | |
252 | 由堆的定义可以看出,堆顶元素(即第一个元素)必为最大项。完全二叉树可以 |
253 | 很直观地表示堆的结构。堆顶为根,其它为左子树、右子树。 |
254 | 初始时把要排序的数的序列看作是一棵顺序存储的二叉树,调整它们的存储顺序, |
255 | 使之成为一个堆,这时堆的根节点的数最大。然后将根节点与堆的最后一个节点 |
256 | 交换。然后对前面(n-1)个数重新调整使之成为堆。依此类推,直到只有两个节点 |
257 | 的堆,并对它们作交换,最后得到有n个节点的有序序列。 |
258 | |
259 | 从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素 |
260 | 交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数 |
261 | 实现排序的函数。 |
262 | |
263 | 堆排序是不稳定的。算法时间复杂度O(nlog2n)。 |
264 | |
265 | */ |
266 | /* |
267 | 功能:渗透建堆 |
268 | 输入:数组名称(也就是数组首地址)、参与建堆元素的个数、从第几个元素开始 |
269 | */ |
270 | void sift( int *x, int n, int s) |
271 | { |
272 | int t, k, j; |
273 | |
274 | t = *(x+s); /*暂存开始元素*/ |
275 | k = s; /*开始元素下标*/ |
276 | j = 2*k + 1; /*右子树元素下标*/ |
277 | |
278 | while (j<n) |
279 | { |
280 | if (j<n-1 && *(x+j) < *(x+j+1)) /*判断是否满足堆的条件:满足就继续下一轮比较,否则调整。*/ |
281 | { |
282 | j++; |
283 | } |
284 | |
285 | if (t<*(x+j)) /*调整*/ |
286 | { |
287 | *(x+k) = *(x+j); |
288 | k = j; /*调整后,开始元素也随之调整*/ |
289 | j = 2*k + 1; |
290 | } |
291 | else /*没有需要调整了,已经是个堆了,退出循环。*/ |
292 | { |
293 | break ; |
294 | } |
295 | } |
296 | |
297 | *(x+k) = t; /*开始元素放到它正确位置*/ |
298 | } |
299 | |
300 | |
301 | /* |
302 | 功能:堆排序 |
303 | 输入:数组名称(也就是数组首地址)、数组中元素个数 |
304 | */ |
305 | void heap_sort( int *x, int n) |
306 | { |
307 | int i, k, t; |
308 | int *p; |
309 | |
310 | for (i=n/2-1; i>=0; i--) |
311 | { |
312 | sift(x,n,i); /*初始建堆*/ |
313 | } |
314 | |
315 | for (k=n-1; k>=1; k--) |
316 | { |
317 | t = *(x+0); /*堆顶放到最后*/ |
318 | *(x+0) = *(x+k); |
319 | *(x+k) = t; |
320 | sift(x,k,0); /*剩下的数再建堆*/ |
321 | } |
322 | } |
323 | |
324 | |
325 | void main() |
326 | { |
327 | #define MAX 4 |
328 | int *p, i, a[MAX]; |
329 | |
330 | /*录入测试数据*/ |
331 | p = a; |
332 | printf ( "Input %d number for sorting :\n" ,MAX); |
333 | for (i=0; i<MAX; i++) |
334 | { |
335 | scanf ( "%d" ,p++); |
336 | } |
337 | printf ( "\n" ); |
338 | |
339 | /*测试选择排序*/ |
340 | |
341 | |
342 | p = a; |
343 | select_sort(p,MAX); |
344 | /**/ |
345 | |
346 | |
347 | /*测试直接插入排序*/ |
348 | |
349 | /* |
350 | p = a; |
351 | insert_sort(p,MAX); |
352 | */ |
353 | |
354 | |
355 | /*测试冒泡排序*/ |
356 | |
357 | /* |
358 | p = a; |
359 | insert_sort(p,MAX); |
360 | */ |
361 | |
362 | /*测试快速排序*/ |
363 | |
364 | /* |
365 | p = a; |
366 | quick_sort(p,0,MAX-1); |
367 | */ |
368 | |
369 | /*测试堆排序*/ |
370 | |
371 | /* |
372 | p = a; |
373 | heap_sort(p,MAX); |
374 | */ |
375 | |
376 | for (p=a, i=0; i<MAX; i++) |
377 | { |
378 | printf ( "%d " ,*p++); |
379 | } |
380 | |
381 | printf ( "\n" ); |
382 | system ( "pause" ); |
383 | } |