排序算法可以分为内部排序和外部排序, 内部排序是数据记录在内存中进行排序, 而外部排序是因为排序的数据很大, 一次不能容纳全部的排序记录, 在排序过程中需要访问外存.
常见的内部排序算法有: 冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序等.
算法一: 冒泡排序
介绍
冒泡排序是一种简单的排序算法, 这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端. 冒泡排序的时间复杂度为O(n^2). 冒泡排序是与插入排序拥有相等的运行时间, 但是两种算法在需要的交换次数却很大地不同. 在最好的情况, 冒泡排序需要O(n^2)次交换,而插入排序只要最多O(n)交换.
复杂度和稳定性
最差时间复杂度 O(n^2)
最优时间复杂度 O(n)
平均时间复杂度 O(n^2)
最差时间复杂度 O(n) total, O(1) auxiliary
稳定性 稳定
步骤
- 取nums[0], 与nums1进行比较, 如果nums[0]大于nums1, 那么nums[0]与nums1交换位置
- 重复步骤1的操作, 两两比较移动, nums1和nums2, nums2和nums3, 直到将最大的数移动到n-1的位置
- 重复步骤1和2, 将第二大的数移动到n-2的位置, 第三大的数移动到n-3的位置, 以此类推, 直到将第n-2大的数移动到1的位置, 最小的数在0位置不动
可以结合步骤看看示意图, 加强理解
代码
#include <stdio.h>
void bubble_sort(int arr[], int len) {
int i, j, temp;
for (i = 0; i < len - 1; i++)
for (j = 0; j < len - 1 - i; j++)
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
int main() {
int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };
int len = (int) sizeof(arr) / sizeof(*arr);
bubble_sort(arr, len);
int i;
for (i = 0; i < len; i++)
printf("%d ", arr[i]);
return 0;
}
算法二: 选择排序
介绍
选择排序的主要优点与数据移动有关. 如果某个元素位于正确的最终位置上, 则它不会被移动. 选择排序每次交换一对元素, 它们当中至少有一个将被移到其最终位置上, 因此对n个元素的表进行排序总共进行至多n-1次交换. 在所有的完全依靠交换去移动元素的排序方法中, 选择排序属于非常好的一种.
复杂度
最差时间复杂度 O(n^2)
最优时间复杂度 O(n^2)
平均时间复杂度 O(n^2)
最差空间复杂度 O(n) total, O(1) auxiliary
稳定性 稳定
步骤
- 从未排序数列中取起始位置的元素, 比较未排序数列找出最小的元素, 并放置在未排序数列的起始位置上
- 重复步骤1, 直到未排序数列只有一个元素
可以结合步骤看看示意图, 加强理解
代码
void selection_sort(int arr[], int len) {
int i, j, min, temp;
for (i = 0; i < len - 1; i++) {
min = i;
for (j = i + 1; j < len; j++)
if (arr[min] > arr[j])
min = j;
if (min != i) {
temp = arr[min];
arr[min] = arr[i];
arr[i] = temp;
}
}
}
算法三: 插入排序
介绍
插入排序在实现上, 通常采用in-place排序(即只需用到O(1)的额外空间的排序), 因而在从后向前扫描过程中, 需要反复把已排序元素逐步向后挪位, 为最新元素提供插入空间.
复杂度
最差时间复杂度 O(n^2)
最优时间复杂度 O(n)
平均时间复杂度 O(n^2)
最差空间复杂度 O(n) total, O(1) auxiliary
稳定性 稳定
步骤
- 从第一个元素开始, 该元素可以认为已经被排序好
- 取出下一个元素, 在已经排序的元素序列中从后向前扫描
- 如果该元素( 已经排序 )大于新元素, 将该元素移动到下一个位置
- 重复步骤3, 知道找到已经排序的呀un苏小于或者等于新元素的位置
- 将新元素插入到该位置后面
- 重复步骤2~5
可以结合步骤看看示意图, 加强理解
代码
void insertion_sort(int array[], int first, int last){
int i, j, temp;
for (i = first + 1; i <= last; i++){
temp = array[i]; //与已经排序好的数逐一比较,大于temp时,该数向后移
for(j = i - 1; j >= first && array[j] > temp; j--) //当first=0, j循环到-1时, 由于[[短路求值]], 不会运算array[-1]
array[j + 1] = array[j];
array[j] = temp; //被排序数放到正确的位置
}
}
算法四: 希尔排序
介绍
希尔排序, 也称递减增量排序算法, 实质是分组插入排序. 希尔排序的基本思想是: 在每一组数列中对列分别进行插入排序, 重复这个过程, 不过每次用更长的列( 或者说更小的组 )来进行.
复杂度和稳定性
最坏时间复杂度 O(n^2)
最优时间复杂度 O(n^1.3)
平均时间复杂度 O(nlogn) ~ O(n^2)
最坏空间复杂度 O(n) total, O(1) auxiliary
稳定性 不稳定
步骤
- 初始化步长数组
- 根据步长数组给数列分组, nums[0]~nums[t-1], nums[t]~nums[2t-1], nums[2t]~nums[3t-1]. . . . . .
- 对数列分组进行列的插入排序, nums[0], nums[t], nums[2t]. nums1, nums[t+1], nums[2t+1]. . . . . .
- 重复步骤2~步骤3, 知道步长为1
可以结合步骤看看示意图, 加强理解
代码
void shellsort2(int a[], int n)
{
int j, gap;
for (gap = n / 2; gap > 0; gap /= 2)
for (j = gap; j < n; j++)//从数组第gap个元素开始
if (a[j] < a[j - gap])//每个元素与自己组内的数据进行直接插入排序
{
int temp = a[j];
int k = j - gap;
while (k >= 0 && a[k] > temp)
{
a[k + gap] = a[k];
k -= gap;
}
a[k + gap] = temp;
}
}
算法五: 归并排序
介绍
归并排序是创建在归并操作上的一种有效的排序算法, 该算法是采用分治法的一个非常典型的应用, 且各层分治递归可以同时进行.
时间复杂度和稳定性
最差时间复杂度 O(nlogn)
最优时间复杂度 O(n)
平均时间复杂度 O(nlogn)
最差时间复杂度 O(n) total
稳定性 稳定
步骤
- 申请空间, 使其大小为两个已经排序序列之和, 该空间用来存放合并后的序列
- 设定两个指针, 最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素, 选择相对小的元素放入到合并空间, 并移动指针到下一个位置
- 重复步骤3知道某个指针到达序列列尾
- 将另一个序列剩下的所有元素直接复制到合并序列列尾
可以结合步骤看看示意图, 加强理解
代码
void merge_sort(int array[], unsigned int first, unsigned int last)
{
int mid = 0;
if(first<last)
{
/*mid = (first+last)/2;*/ /*注意防止溢出*/
/*mid = first/2 + last/2;*/
/*mid = ((first & last) + (first ^ last) >> 1);*/
/*mid = ((first & last) + (first ^ last) >> 1);*/ /*修正上一句“=”右侧中文左括号错误*/
mid = ((first & last) + ((first ^ last) >> 1)); /*修正上一句优先级错误*/
merge_sort(array, first, mid);
merge_sort(array, mid+1,last);
merge(array,first,mid,last);
}
}
void merge_sort(int *list, int length){
int i, left_min, left_max, right_min, right_max, next;
int *tmp = (int*)malloc(sizeof(int) * length);
if (tmp == NULL){
fputs("Error: out of memory\n", stderr);
abort();
}
for (i = 1; i < length; i *= 2)
for (left_min = 0; left_min < length - i; left_min = right_max){
right_min = left_max = left_min + i;
right_max = left_max + i;
if (right_max > length)
right_max = length;
next = 0;
while (left_min < left_max && right_min < right_max)
tmp[next++] = list[left_min] > list[right_min] ? list[right_min++] : list[left_min++];
while (left_min < left_max)
list[--right_min] = list[--left_max];
while (next > 0)
list[--right_min] = tmp[--next];
}
free(tmp);
}
算法六: 快速排序
介绍
快速排序使用分治法策略来把一个序列分为两个子序列.
时间复杂度和稳定性
最差时间复杂度 O(n^2)
最优时间复杂度 O(nlogn)
平均时间复杂度 O(nlogn)
最差空间复杂度 根据实现的方式不同而不同
稳定性 不稳定
步骤
- 从数列中跳出一个元素, 称为"基准"
- 重新排列数列, 所有比基准小的元素放在基准前面, 所有比基准大的元素放在基准后面( 相同的数可以放在任意一边 ). 在这个分区结束后, 该基准就处于数列的中间位置. 这个称为分区操作.
- 递归地把小于基准值元素的子数列和大于基准值元素的子数列进行排序
可以结合步骤看看示意图, 加强理解
代码
//交换位置
void swap(int *a , int *b)
{
int tmp = *a ;
*a = *b ;
*b = tmp;
}
//分治
int partition(int *ary, int len , int pivot_i )
{
int i = 0 ;
int small_len = pivot_i ;
int pivot = ary[pivot_i] ;
swap(&ary[pivot_i],&ary[pivot_i+(len-1)]);
for(; i < len; i++)
{
if(ary[pivot_i+i]<pivot)
{
swap(&ary[pivot_i+i],&ary[small_len]) ;
small_len++ ;
}
}
swap(&ary[pivot_i+len-1],&ary[small_len]) ;
return small_len;
}
void quick_sort(int *ary, int len)
{
if(len==0||len==1)
return ;
int small_len = partition (ary,len,0) ;
quick_sort (ary,small_len) ;
quick_sort(&ary[small_len+1],len-small_len-1) ;
}
int main(void) {
int ary[] = {2,4,2,5,3,5,3,1,7,6};
int len = sizeof(ary) / sizeof(ary[0]);
quick_sort(ary, len);
return 0;
}
算法七: 堆排序
介绍
堆排序是指利用堆这种数据结构所设计的一种排序算法. 堆积是一个近似完全二叉树的结构, 并同时满足堆积的性质: 即子结点的键值或索引总是小于(或者大于)它的父节点.
时间复杂度和稳定性
最差时间复杂度 O(nlogn)
最优时间复杂度 O(nlogn)
平均时间复杂度 O(nlogn)
最差空间复杂度 O(n) total, O(1) auxiliary
稳定性 不稳定
步骤
堆节点的访问
- 父节点i的左子结点在位置(2*i+1)
- 父节点i的右子节点在位置(2*i+2)
- 父节点i的父节点在位置floor((i-1)/2)
堆的操作
在堆的数据结构中, 堆中的最大值总是位于根节点. 堆中定义以下几种操作.
- 最大堆调整: 将堆的末端子节点作调整, 使得子节点永远小于父节点.
- 创建最大堆: 将堆所有数据重新排序.
- 堆排序: 移除位在第一个数据的根节点, 并做最大堆调整的递归运算.
可以结合步骤看看示意图, 加强理解
代码
#define MAX_HEAP_LEN 100
static int heap[MAX_HEAP_LEN];
static int heap_size = 0; // the number of elements in heaps
static void swap(int* a, int* b)
{
int temp = *b;
*b = *a;
*a = temp;
}
static void shift_up(int i)
{
int done = 0;
if( i == 0) return; //node is the root already
while((i!=0)&&(!done))
{
if(heap[i] > heap[(i-1)/2])
{//if the current is larger than the parent, then swap
swap(&heap[i],&heap[(i-1)/2]);
}
else
{// the job is already done.
done =1;
}
i = (i-1)/2;
}
}
static void shift_down(int i)
{
int done = 0;
if (2*i + 1> heap_size) return; // node i is a leaf
while((2*i+1 < heap_size)&&(!done))
{
i =2*i+1; // jump to left child
if ((i+1< heap_size) && (heap[i+1] > heap[i]))
{// find the bigger one of the two children
i++;
}
if (heap[(i-1)/2] < heap[i])
{
swap(&heap[(i-1)/2], &heap[i]);
}
else
{
done = 1;
}
}
}
static void delete(int i)
{
int last = heap[heap_size - 1]; // get the last one;
heap_size--; // shrink the heap
if (i == heap_size) return;
heap[i] = last; // use the last item to overwrite the current
shift_down(i);
}
int delete_max()
{
int ret = heap[0];
delete(0);
return ret;
}
void insert(int new_data)
{
if(heap_size >= MAX_HEAP_LEN) return;
heap_size++;
heap[heap_size - 1] = new_data;
shift_up(heap_size - 1);
}