算法一:冒泡排序
每次比较相邻两元素,若错序则交换,多次遍历,直至无交换,实质上需要排序k-1趟,每次找到未排序元素中的最大值放在已排序序列最后面,稳定。
void bubble_sort(int array[], int n)
{
int i, j;
for (i = 0; i < n - 1; i++) //排序n-1趟,每次寻找未排序序列中的最大值
{
for (j = 0; j< n-i-1; j++) //每次循环从头相邻两两比较,将大值升上去
if (array[j] >array[j + 1]) //每趟可能需要交换多次
{
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
改进:增加状态变量flag,若某趟循环无交换发生,则停止继续循环
时间复杂度分析:最好情况,全部有序,在已改进的基础上,比较n-1次,故时间复杂度为O(n),最坏情况,逆序排列,则需要比较n(n-1)/2次,交换n(n-1)/2次,时间复杂度为O(n^2)。
缺点:一次循环内多次交换
算法二:简单选择排序
进行n-1趟排序,每次寻找未排序所有元素的最小值放在已排序序列的最末端,稳定。
void selection_sort(int array[],int n)
{
int i, j, min, temp;
for (i = 0; i < n - 1; i++) //排序n-1趟,每次寻找i位置上的值
{
min= i;
for (j = i + 1; j< n; j++) //i后数据与min依次比,找应放在i位置上的索引给min
if (array[j] <array[min])
min= j;
if (min != i) //每个位置只交换一次
{
temp = array[i];
array[i] = array[min];
array[min] = temp;
}
}
}
时间复杂度分析:无论最好或最坏情况,均需要比较n(n-1)/2次,最好情况无交换,故时间复杂度为O(n),最坏情况交换n-1次,时间复杂度为O(n^2),但性能上还是略优于冒泡排序。
算法三:插入排序
将数据依次插入,对于每次新插入的数据,从末尾开始在已排好序的数列中找到其正确的位置,将其后数据从最后一个依次后移,最后插入。若待插入元素与有序序列中某元素相等,则插入在相等元素的后面,故稳定。
void insert_sort(int array[], int n)
{
int i, j, temp;
for (i = 1; i < n; i++) //i为当前要插入的数据索引,开始时均插在最后
{
temp= array[i]; //备份要插入的值
for (j = i; j > 0&& array[j - 1] > temp; j--) //将i前数据与i比较,后移空位置
array[j] = array[j - 1];
array[j] = temp; //将i放置该放位置
}
}
时间复杂度分析:最好情况,全部有序,需要比较n-1次,最好情况无交换,故时间复杂度为O(n),最坏情况比较(n+2)(n-1)/2次(注意j循环中有两次比较),移动次数为(n+4)(n-1)/2次,时间复杂度为O(n^2),但性能上还是略优于冒泡排序和简单选择排序。
优势:在基本有序或者记录数目较少的时候
缺点:共需n趟排序,效率低,并且每次循环只能将数据移动一位
算法四:希尔排序
如上述算法三,在记录数目较少时,直接插入排序有优势,若记录数目较多,则可将记录分割成多个子序列,在每个子序列(记录数目较少)内直接插入排序。当整个序列基本有序时,再对全体进行一次直接插入排序。选择一个增量序列,按照增量序列进行k趟排序,每趟排序将整个待排序的记录序列分割成为m个子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时(增量降为1),再对全体记录进行依次直接插入排序,由于跳跃式移动不稳定。
void shell_sort(int array[], int n)
{
int i, j, Increment,temp;
for (Increment = n / 2; Increment >0; Increment /= 2) //计算希尔增量,对应k趟排序
for (i = Increment; i< n; i++) //i为要插入的数据索引,开始时均插在子序列最后
{
temp= array[i];
for (j = i; j >=Increment &&array[j - Increment] > temp; j -=Increment)
//将i在子序列中前面数据依次与i比较, 后移空出位置
array[j] = array[j - Increment];
array[j] = temp;
}
}
时间复杂度分析:取决于增量序列的选择,对于上述序列复杂度为O(n^3/2),第一个突破O(n^2)的算法
算法五:堆排序
直接选择排序并没有把每趟的比较结果记录下来,导致后一趟的比较中有许多前一趟已经做过了,因而比较次数较多,若能找到本趟循环要找的值的同时,对其他记录进行相应的调整,则会提高整体效率。
大顶堆:每个节点的值都大于或等于左右孩子结点的值的完全二叉树
小顶堆:每个节点的值都小于或等于左右孩子结点的值的完全二叉树
将待排序的序列构造成一个大顶堆,根结点则为最大值,将其与堆数组末尾元素交换,再将剩余n-1个序列重新构成一个大顶堆,如此反复循环,便能得到一个有序序列。故需解决两个问题:如何构建堆?输出堆顶后,如何调整成新的堆?
void heapadjust(int *a, int start, int end)
{
int temp = a[start]; //当前根结点
while( 2 * start + 1 < end)//当前根结点的左孩子
{
int i = 2 * start + 1;
if (i + 1 < end && a[i + 1] > a[i]) // 比较两个孩子中的较大值
i++;
if (a[i] < temp)
break;
a[start] = a[i]; //将孩子与根结点交换
start = i;
}
a[start] = temp;
}
void heapSort(int a[], int n)
{
for (int i = (n - 1) / 2; i >= 0; i--)//n - 1/2为最后一个双亲结点
heapadjust(a, i, n); //从最后一个双亲结点开始逐步向上调整为大顶堆
for (int i = n -1 ; i > 0; i--)
{
int temp = a[i];
a[i] = a[0];
a[0] = temp;//将堆顶结点与最后一个元素交换
heapadjust(a, 0, i);//将剩余元素调整为大顶堆
}
}
时间复杂度分析:时间消耗在初始建堆和重建堆的反复筛选,从最下层最右边非端点开始与左右孩子比较,最多两次比较和互换,因此构建堆为O(n);正式排序时,第i次取堆顶记录重建堆需要O(logi)的时间(完全二叉树的某个结点到根结点距离为[logi]+1),并且需要取n-1次,因此重建堆复杂度为O(nlogn),整体时间复杂度为O(nlogn)
特点:对原始记录排序状态不敏感,跳跃式交换不稳定。由于初始建堆所需比较次数较多,不适合待排序个数较少的情况
算法六:归并排序
采用分治思想,对任一序列,分成左右子列,子列内部排序,递归实现,然后用指针移动合并两子列实现归并操作,稳定。
代码实现:
void Merge(int array[],int tempArr[], int startIndex,int midIndex,int endIndex)
{
int i = startIndex, j = midIndex + 1, k = startIndex;
while (i != midIndex + 1 && j !=endIndex + 1) //直至有一子列全部排进合并列
{
if (array[i] >array[j])
tempArr[k++] = array[j++];
else
tempArr[k++] = array[i++];
}
while (i != midIndex + 1) //未排完子列剩余元素直接送人合并列
tempArr[k++] = array[i++];
while (j != endIndex + 1)
tempArr[k++] = array[j++];
for (i = startIndex; i <= endIndex; i++) //将合并列放入原序列应有的位置
array[i] = tempArr[i];
}
void mergeSort(intarray[],inttempArr[], intstartIndex,intendIndex)
{
int midIndex;
if (startIndex <endIndex)
{
midIndex= (startIndex +endIndex) / 2;
mergeSort(array, tempArr, startIndex, midIndex); //对左子列排序
mergeSort(array, tempArr, midIndex + 1, endIndex); //对右子列排序,注意加1
Merge(array, tempArr, startIndex, midIndex, endIndex); //归并操作合并两子列
}
}
*** 需要提前申请临时空间tempArr[]存放合并列,分治思想降低了时间复杂度,但递归实现提高了空间复杂度
算法七:快速排序--目前最受推崇的排序算法
取序列中任一元素作为枢纽元,将其余元素与该枢纽元比较,分成大小两个集合,等于枢纽元的元素随机分配,递归地对两个集合快速排序。
根据细节不同,有不同的方法,标准算法取第一个数做枢纽元,这样在数组已经有序的情况下,每次划分将得到最坏的结果。一种比较常见的优化方法是随机化算法,即随机选取一个元素作为主元。这种情况下虽然最坏情况仍然是O(n^2),但最坏情况不再依赖于输入数据,而是由于随机函数取值不佳。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。随机化快速排序的唯一缺点在于,一旦输入数据中有很多的相同数据,随机化的效果将直接减弱。最好的方法是取左端,右端和中间位置上的三个元素的中值做枢纽元。取这3个值的好处是在实际问题中,出现近似顺序数据或逆序数据的概率较大,此时中间数据必然成为中值,而也是事实上的近似中值。万一遇到正好中间大两边小(或反之)的数据,取的值都接近最值,那么由于至少能将两部分分开,实际效率也会有2倍左右的增加,而且利于将数据略微打乱,破坏退化的结构。
代码实现:
int Median3(intarray[],int left, int right)
{
int center = (left +right) / 2;
if (array[left] >array[center]) //将左端,右端,中间三值排序
swap(&array[left], &array[center]);
if (array[left] >array[right])
swap(&array[left], &array[right]);//三者中最小值放左端且无需再判断
if (array[center] >array[right])
swap(&array[center], &array[right]);//三者中最大值放右端且无需再判断
swap(&array[center], &array[right-1]);//三者中中间值放倒数第二个做枢纽元
return array[right - 1];
}
#define cutoff 3
void QuickSort(intarray[],int left, int right)
{
if (left +cutoff <=right)
{
int pivot = Median3(array,left,right);
int i = left;
int j = right - 1;
while (1)
{
while (array[++i] < pivot) {}//从两侧分别找到错误分组的元素
while (array[--j] > pivot) {}
if (i < j) //若i,j尚未交错
swap(&array[i], &array[j]);
else
break; //当前分组正确
}
swap(array[i], array[right - 1]); //把枢纽元放到中间
QuickSort(array, left, i - 1);
QuickSort(array, i + 1,right);
}
else
InsertionSort(array + left, right - left + 1);//对于小数组,插入排序更合适
}
*** 与归并相比,同样是递归,可是分成的两个子问题并不一定相等(隐患),但分成两组是在适当的位置进行且非常有效,故比递归更快。
算法八:基数排序
基数排序用于对多关键字域数据(例如:一副扑克牌,大小可以看做一个关键字域,花色也可以看做另一个关键字域)进行排序,每次对数据按一种关键字域进行排序,然后将该轮排序结果按该关键字的大小顺序堆放,依次进行其他关键字域的排序,最后实现序列的整体排序。
限制:
基数排序需要一种稳定的排序算法作为子程序,在这里使用计数排序。
时间复杂度:
(1)给定n个d位k进制数,使用计数排序(耗时:)作为子程序,那么时间复杂度为:
,因此,当d为常数,
时,为线性代价;
(2)给定n个b位k进制数,若b太大,可考虑将b分成r段,这时得到n个b/r位k^r进制数,同样使用计数排序(耗时:),那么时间复杂度为:
,当
时,此时时间复杂度为:
。
缺点:
不是原址排序;虽然可以达到线性时间复杂度,但是常数因子较大。