插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
算法步骤:
1)将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
2)从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
代码部分:
void InsertSort(int *p, int n)
{
int i = 0, j = 0, t = 0, count1 = 0, count2 = 0;
for(i=1; i<n; i++)
{
t = p[i]; //临时保存待插入的元素,一般从数组第二个有效元素开始,第一个有效元素表示一个有序序列
if(p[i] < p[i-1])
for(j=i; j>0; j--)
{
count1++; //标记进行了多少次比较
//比较待插入元素在数组前i-1个有序元素中的大小,然后插入;“<”符号表示把小的元素(t)移动到前面(升序),“>”表示把大的元素(t)移动到前面(降序)
if(t < p[j-1])
{
count2++;//标记进行了多少次交换
p[j] = p[j-1];
p[j-1] = t;
}
}
}
printf("比较了%d次,交换了%d次\n\n", count1, count2);
}
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率
但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
算法步骤:
1)选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
2)按增量序列个数k,对序列进行k 趟排序;
3)每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
代码部分:
void ShelltSort(int *p, int n)
{
int i = 0, j = 0, t = 0, space = 0, count1 = 0, count2 = 0;
space = n;//初始化递减增量值
while(space > 1)
{
space = space/5 + 1;
for(i=space; i<n; i+=space)
{
t = p[i]; //临时保存待插入的元素
if(p[i] < p[i-1])
for(j=i; j>0; j-=space)
{
count1++; //标记进行了多少次比较
if(t < p[j-space])
{
count2++;//标记进行了多少次交换
p[j] = p[j-space];
p[j-space] = t;
}
}
}
}
printf("比较了%d次,交换了%d次\n\n", count1, count2);
}
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
算法步骤:
1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3)针对所有的元素重复以上的步骤,除了最后一个。
4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
代码部分:
void BubbleSort(int *p, int n)
{
int i = 0, j = 0, t = 0, flag = 1, count1 = 0, count2 = 0;
for(i=0; i<n-1 && flag; i++)
{
flag = 0; //如果下面循环中没有执行交换,表示这趟元素是有序的
for(j=0; j<n-i-1; j++)//n-i再减1是防止数组越界,因为下面是用的j+1
{
count1++; //标记进行了多少次比较
if(p[j] > p[j+1]) //相邻两个元素相比较,<排序后元素递减,>排序后元素递增
{
t = p[j];
p[j] = p[j+1];
p[j+1] = t;
flag = 1; //如果进行了交换说明元素不是有序的,还需要进行下一趟冒泡
count2++; //标记进行了多少次交换
}
}
}
printf("比较了%d次,交换了%d次\n\n", count1, count2);
}
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
算法步骤:
1 从数列中挑出一个元素,称为 “基准”(pivot),
2 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
代码部分:
//调用
QuickSort(array, 0, num-1);
//快速排序
void QuickSort(int *p, int low, int high)
{
if(low >= high) return; //递归终止条件,即不能再分裂序列为止
int i = low, j = high, t = 0;
while(i < j)
{
while(i < j && p[j] >= p[low])//从右往左找小于基准值的数
{
count1++;//标记进行了多少次比较
j--;
}
while(i < j && p[i] <= p[low])//从左往右找大于基准值的数
{
count1++;//标记进行了多少次比较
i++;
}
t = p[i]; //交换找到的两个数,即排序
p[i] = p[j];
p[j] = t;
count2++; //标记进行了多少次交换
}
t = p[low]; //将基准值归位,使得序列中基准值左边的数都大于(或小于)基准值右边的数
p[low] = p[i];
p[i] = t;
count2++; //标记进行了多少次交换
QuickSort(p, low, i-1);//递归调用,将基准值左边序列排序
QuickSort(p, i+1, high);//将基准值右边的序列排序
}
选择排序(Selection sort)也是一种简单直观的排序算法。
算法步骤:
1)首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
2)再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
3)重复第二步,直到所有元素均排序完毕。
代码部分:
void SelectSort(int *p, int n)
{
int i = 0, j = 0, t1 = 0, t2 = 0, count1 = 0, count2 = 0;
for(i=0; i<n-1; i++)
{
t1 = i;
for(j=i+1; j<n; j++)
{
count1++; //标记比较了多少次
if(p[t1] > p[j])//“>”符号表示:t1选择保存最小的元素下标,排序后为升序;“<”符号表示:t1选择保存最大元素的下标,排序后为降序
t1 = j;
}
if(i != t1) //i != t1表示进行了下标交换,说明找到了比小标i对应的元素更小或者更大的元素,则需要将t1和i下标对应的元素交换
{
t2 = p[t1];
p[t1] = p[i];
p[i] = t2;
count2++; //标记交换了多少次
}
}
printf("比较了%d次,交换了%d次\n\n", count1, count2);
}
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆排序的平均时间复杂度为Ο(nlogn) 。
算法步骤:
1)创建一个堆H[0…n-1]
2)把堆首(最大值)和堆尾互换
3)把堆的尺寸缩小1,并调用shift_down(0),目的是把新的数组顶端数据调整到相应位置
4) 重复步骤2,直到堆的尺寸为1
代码部分:
void HeapSort(int *p, int n)
{
int i = 0, t = 0;
for(i=n/2; i>=0; i--)//i=n/2表示从完全二叉树最后一个有叶子节点的节点开始向堆顶创建大根堆(或者小根堆)
{
CreateHeap(p, i, n);
}
for(i=n-1; i>=0; i--)//将已经构建好的大根堆(或小根堆)进行排序,把大的(小的)元素放在数组尾(从后往前存放)
{
//交换堆顶和最后一个待排序的元素,则堆顶表示的最大(或最小)元素交换到了数组最后去
t = p[i];
p[i] = p[0];
p[0] = t;
//因为交换了元素,不满足大根堆(或小根堆)的定义,所以重建大根堆(或者小根堆),将次大或者次小的元素移动堆顶
CreateHeap(p, 0, i);//交换的是根节点,所以从根节点开始重建
}
printf("比较了%d次,交换了%d次\n\n", count1, count2);
}
void CreateHeap(int *p, int k, int n)
{
int i = 0, t = 0;
t = p[k];//保存待调整的元素
for(i=k*2+1; i<n; i=i*2+1)//k*2+1和i*2+1表示指向其左孩子
{
count1++;
if(i < n-1 && p[i+1] > p[i])//(i
i++;
if(p[i] <= t)//如果左右孩子中最大的都比其根节点小,说明已经是个大根堆(<创建大根堆,>创建小根堆),就退出循环
break;
count2++; //标记进行了多少次交换
p[k] = p[i]; //把大的元素移动到其双亲节点的位置
k = i; //更改k指向的位置,表示待待调整元素改在的位置
}
p[k] = t;//循环完上面后,k指向大根堆(小根堆)元素t该在的位置,然后把t存放进去
}
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
算法步骤:
申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
设定两个指针,最初位置分别为两个已经排序序列的起始位置
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针达到序列尾
将另一序列剩下的所有元素直接复制到合并序列尾
代码部分:
/*
归并排序,递归实现
*/
#include
#include
#define num 10 //数组元素个数
int count1 = 0, count2 = 0;
void MergeSort(int *p, int low, int high);//传入待排序数组和数组元素的个数
void Merge(int *p, int low, int min, int high);//将对应数据分为两个序列,排序后合并为一个序列
int main()
{
int i = 0, array[num] = {5,2,6,0,3,9,1,7,4,8};
MergeSort(array, 0, num-1);
printf("归并序后为:");
for(i=0; i<num; i++)
printf("%d ", array[i]);
printf("\n\n比较了%d次,交换了%d次\n", count1, count2);
return 0;
}
void MergeSort(int *p, int low, int high)
{
if(p == NULL || low >= high)//数据为空或者区间已经分裂到一个元素时突出,即递归终止条件
return;
int min = (low + high)/2; //指向中间位置
MergeSort(p, low, min);
MergeSort(p, min+1, high);
Merge(p, low, min, high);
}
void Merge(int *p, int low, int min, int high)
{
int i = low, j = min+1, k = 0, *temp;
temp = (int*)malloc((high-low+1)*sizeof(int*));//申请临时空间
while(i<=min && j<=high)//把两个有序序列组合成一个有序序列
{
count1++; //标记进行了多少次比较
if(p[i] <= p[j])//将小的数据存放在进temp,“<”符号为升序排序,“>”为降序排序
temp[k++] = p[i++];
else
temp[k++] = p[j++];
}
while(i <= min) //将剩余的数据依次存放进临时数组,i下标对应的剩余的数据一定大于(或小于)j下标对应的所有数据
{
// count2++; //标记进行了多少次交换
temp[k++] = p[i++];
}
while(j <= high)//如果j下标对应的数据有剩余,表示j下标对应的剩余的数据一定大于(或小于)i下标 对应的所有数据
{
// count2++; //标记进行了多少次交换
temp[k++] = p[j++];
}
for(i=0; i<k; i++)//排序完成把所有的数据对应的赋给原来的数组
{
count2++; //标记进行了多少次交换
p[low+i] = temp[i];
}
free(temp); //释放空间
}
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
说基数排序之前,我们简单介绍桶排序:
算法思想:是将阵列分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的阵列内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。
简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的在进行排序。
例如要对大小为[1…1000]范围内的n个整数A[1…n]排序
首先,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1…10]的整数,集合B[2]存储 (10…20]的整数,……集合B[i]存储( (i-1)10, i10]的整数,i = 1,2,…100。总共有 100个桶。
然后,对A[1…n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。 再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任 何排序法都可以。
最后,依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这 样就得到所有数字排好序的一个序列了。
假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。如果
对每个桶中的数字采用快速排序,那么整个算法的复杂度是
O(n + m * n/m*log(n/m)) = O(n + nlogn – nlogm)
从上式看出,当m接近n的时候,桶排序复杂度接近O(n)
当然,以上复杂度的计算是基于输入的n个数字是平均分布这个假设的。这个假设是很强的 ,实际应用中效果并没有这么好。如果所有的数字都落在同一个桶中,那就退化成一般的排序了。
前面说的几大排序算法 ,大部分时间复杂度都是O(n2),也有部分排序算法时间复杂度是O(nlogn)。而桶式排序却能实现O(n)的时间复杂度。但桶排序的缺点是:
1)首先是空间复杂度比较高,需要的额外开销大。排序有两个数组的空间开销,一个存放待排序数组,一个就是所谓的桶,比如待排序值是从0到m-1,那就需要m个桶,这个桶数组就要至少m个空间。
2)其次待排序的元素都要在一定的范围内等等。
代码部分:
int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
int d = 1; //保存最大的位数
int p = 10;
for(int i = 0; i < n; ++i)
{
while(data[i] >= p)
{
p *= 10;
++d;
}
}
return d;
}
void radixsort(int data[], int n) //基数排序
{
int d = maxbit(data, n);
int *tmp = newint[n];
int *count = newint[10]; //计数器
int i, j, k;
int radix = 1;
for(i = 1; i <= d; i++) //进行d次排序
{
for(j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空计数器
for(j = 0; j < n; j++)
{
k = (data[j] / radix) % 10; //统计每个桶中的记录数
count[k]++;
}
for(j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for(j = 0; j < n; j++) //将临时数组的内容复制到data中
data[j] = tmp[j];
radix = radix * 10;
}
delete[]tmp;
delete[]count;
}