本文中将介绍七大排序算法中的三种排序:冒泡排序,选择排序,插入排序。剩余的排序算法将在接下来的文章中一一介绍。
介于排序分为升序和降序两种,所以,这里将比较规则以回调函数的方式传入排序算法中,这样,在具体实现时,就可以根据不同的需求传递不同的函数指针进而达到不同的排序效果。
升序比较规则定义如下:
//升序比较规则 int Greater(int a,int b) { return a > b? 1 : 0; }
降序比较规则定义如下:
//降序比较规则 int Less(int a,int b) { return a < b? 1 : 0; }
将排序规则以回调函数的方式呈现:
typedef int(*Compare)(int a,int b);
在下文的多个排序算法中多处用到交换函数,所以,这里现在这里定义好:
void Swap(int* a,int* b) { int temp = *a; *a = *b; *b = temp; return; }
在接下来的算法中,均以升序为例进行说明。
1. 冒泡排序
冒泡排序的思想是在一轮一轮的比较中:
如果是升序排序:将每一轮排序中的最大的数依次放在最高位,次高位...(从前往后冒),或者在每一轮排序中将最小的数依次放在最低位,次低位,...(从后往前冒)。经过多轮排序之后,待排序序列已经按升序的序列排序好。
如果是降序排列:同理,将每一轮中将最小的数从前往后冒到最高位,次高位,...,或者将最大的数从后往前冒到最低位,次低位,...,。多轮排序之后,即可达到降序的效果。
在上述叙述中,有两种冒泡方法,一种是从前往后冒,一种是从后往前冒,这两种方法的思想是一样的,只是在实现方式方法由略微的差别,以下将给出这两种代码的实现方式,size为初始时待排序序列总的元素个数:
从后往前冒:
[0,bound)为已排序区间,[bound,size)为待排序区间。bound初始从0开始,每次从下标为size-1的数组元素开始,对比相邻两个元素的大小关系,如果前面的大于后面的元素,则交换两元素的值,然后继续往前进行对比,直到对比到bound处,此时,bound处的元素一定是待排序区间中的最小值。
在下一次的比较中,待排序区间中的bound后移一个元素,继续进行上述的比较,等到待排序区间元素为0时,此时,便排序完成。
代码实现如下:
//从后往前冒 void BubbleSort(int arr[],uint64_t size,Compare cmp) { if(arr == NULL || size <= 1) { return; } int bound = 0; for(;bound < size - 1;bound++) { //在每一轮排序中从往前比较 int cur = size - 1; for(;cur > bound;cur--) { if(cmp(arr[cur - 1],arr[cur]) == 1) { Swap(&arr[cur],&arr[cur - 1]); } } } return; }
从前往后冒:
从下标为0开始比较,首先将这个数组元素中的最大值经一轮比较之后放置在最高位,此时带排序区间为[0,size-1);在下一轮比较中,还是从下标为0开始,比较下标从0到次低位下标元素中的最大值,然后放置在次低位,此时待排序区间为:[0,size-2);依次往后...,等到待排序区间为[0,0)时,此时排序完成。
代码实现如下:
//从前往后冒 void BubbleSortEx(int arr[],uint64_t size,Compare cmp) { if(arr == NULL || size <= 1) { return; } //此时,[0,bound]为带排序区间,(bound,size)为已排序区间 int bound = size - 1; for(;bound > 0;bound--) { int cur = 0; for(;cur < bound;cur++) { if(cmp(arr[cur],arr[cur + 1]) == 1) { Swap(&arr[cur],&arr[cur + 1]); } } } return; }
冒泡排序的时间复杂度为:O(n^2),空间复杂度为:O(1),稳定性为稳定。
2. 选择排序
选择排序类似于打擂台的模式。其中,[0,bound)为已排序区间,[bound,size)为带排序区间。初始时,bound为0.
在第一轮比较中,首先将bound处的元素当做擂主,然后将bound处的值与其之后的cur下标处的元素分别进行比较,如果cur处的元素小于bound处的元素,此时交换两元素的值。然后cur后移,直到遍历完待排序区间,此时bound就是待排序区间中最小值。
在下一轮比较中,bound后移一个元素,然后继续上述的操作。待bound等于size时即待排序区间元素个数为0时,排序完成。
这种方法的实现其实和冒泡排序的从后往前冒比较类似。
代码实现如下:
//选择排序:打擂主,边界值每次做擂主,使边界后的各元素与擂主比较 void SelectSort(int arr[],uint64_t size,Compare cmp) { if(arr == NULL || size <= 1) { return; } //[0,bound):表示已排好序的区间 //[bound,size):表示未排好序的区间 int bound = 0; for(;bound < size - 1;bound++) { int cur = bound + 1; for(;cur < size;cur++) { if(cmp(arr[bound],arr[cur]) == 1) { Swap(&arr[bound],&arr[cur]); } } } return; }
选择排序的时间复杂度为:O(n^2),空间复杂度为:O(1),稳定性为不稳定。
3. 插入排序
插入排序的思想是:定义[0,bound)为已排序区间,[bound,size)为待排序区间。bound初始从1开始。
每次在待排序区间中取bound处的值,然后在已排序区间中为bound找到一个合适的位置进行存放。在取出bound处的值之后:定义cur遍历已排序区间,cur初始为bound。
(1) 如果bound处的值大于cur-1处的值,则cur即为bound的存放位置,此时直接将bound处的值存放在cur处即可。
(2)否则说明cur-1处的值必定在bound处的值之后,因此,将cur-1处的值放置在cur处,然后cur--,继续进行(1)中的比较。直到将已排序区间遍历完,即cur等于0时,说明,bound处的值比已排序区间中的元素都小,所以bound处的值应放置在此时的cur处即下标为0的位置。
上述(1)(2)过后,将bound处的值已经插入到待排序区间中。下一次将bound后移,继续对新的bound处的值找到合适存放位置。等到bound为size时,即带排序区间为空时,排序完成。
代码实现如下:
void InsertSort(int arr[],uint64_t size,Compare cmp) { if(arr == NULL || size <= 1) { return; } //[0,bound):表示已排好序的区间 //[bound,size):表示未排序区间 //每次取bound处的值,为bound_value找到合适的位置进行插入 //利用cur进行辅助查找 //如果cur-1处的值大于bound_value,将cur-1处的值后移,然后cur--往前遍历 //当cur - 1处的值小于bound_value时,此时cur的位置就是bound_value的最终位置 //当cur遍历到0处时,cur的位置也是bound_value的最终位置 int bound = 1; for(;bound < size;bound++) { int cur = bound; int bound_value = arr[bound]; for(;cur > 0;cur--) { if(cmp(arr[cur - 1],bound_value) == 1) { arr[cur] = arr[cur - 1]; } else { break; } } arr[cur] = bound_value; } return; }
插入排序的时间复杂度为:O(n^2),空间复杂度为:O(1),稳定性为:稳定
通过上述的描述,可以发现,插入排序有两个特点:
(1)如果待排序序列有序性比较高时,比较的次数就少,所以效率就比较高;
(2)如果待排序列的元素个数较少时,比较的次数也比较少,此时效率也比较高。