先定义好swap函数:
void swap(int *a, int *b)
{
int temp;
temp= *a;
*a = *b;
*b = temp;
}
(1) 冒泡排序
时间:
O(n2)
空间:O(1)
稳定性:稳定
void bubbleStore(int a[], int length)
{
if(a == NULL)
return;
for(int i = 0; i < length-1; i++)
{
for(int j = i+1; j < length; j++)
{
if(a[j] < a[i])
swap(&a[i], &a[j]);
}
}
}
(2) 插入排序
时间:
O(n2)
空间:O(1)
稳定性:稳定
void insertStore(int a[],int length)
{
if(a == NULL)
return;
for(int i = 1; i < length; i++)
{
int toInsert = a[i]; //即将插入到有序序列中的数
for(int j = i-1; j >= 0 && toInsert < a[j]; j--) //有序序列
{
//一步一步地把toInsert往前移动,直到移到合适的位置,这与交换两个数值是不一样的!!
a[j+1] = a[j];
a[j] = toInsert;
}
}
}
插入排序与冒泡排序的区别:冒泡排序是从后n(n的初始值即为数组长度)个数中找到最小的放到前面,然后将n所界定的范围缩小。插入排序是保证前n个数有序(方法是将第n个数插入到前n个已经排好序的数中,故曰插入排序),n从0开始,n逐渐增大即可将整个数组排序。
(3) 快速排序
-
时间:O(n·log n),总体最快
空间:O(log n),但由于是递归的,对于内存非常有限的机器不是一个好的选择
稳定性:不稳定
原理:找一个分割点,比他小的数移到一边,比他大的数移到另一边 性能:
void quickSort(int a[], int length)
{
if(a == NULL || length < 2)
return;
int partPoint = length/2 - 1; //空间:O(log n),也可使用随机数
swap(&a[partPoint], &a[length-1]); //分割点换到最后
int smallerCount = 0; //每把比分割点小的数换到前面,其值+1
for(int i = 0; i < length -1; i++) //注意这里i不能从1开始
{
if(a[i] < a[length-1]) //如果a[i]比分割点(a[length-1])小,则把a[i]移到前面(比分割点小的数集中在a[]的前面一部分,把a[i]加到这部分的末位)
{
swap(&a[smallerCount], &a[i]);
smallerCount++;
}
}
swap(&a[smallerCount], &a[length-1]); //把分割点换回到两部分的交界处
quickStore(a, smallerCount); //排分割点的前半部分
int offset = smallerCount + 1; //前面这么多个数不用排了,故偏移量是这么多
quickStore(a+offset, length-offset); //排分割点的后半部分
}
(4) 希尔排序
基本思想:插入排序的升级版(根据其特点:序列大部分已排好序时效率很高),将数据按逐渐减小的步长gap分组(每组元素下标满足b+a·gap,每组内b>=0为常数,a从0开始自然增大,如第二组的b=1,则第一组下标为(1,1+gap,1+2gap,1+3gap,……)),分别对每一组进行排序,然后对所有元素进行一次排序(即最后步长必须为1),步长的选择是递减的,比如5、3、1,现在一般使用D.E.Knuth分组方法(n很大是,用h(n+1)=3h(n)+1来分组,即1、4、13……)。
-
性能:
-
时间:O(n·log n)
空间:O(1)
稳定性:不稳定
Shell排序比冒泡排序快5倍,比插入排序大致快2倍。Shell排序比起QuickSort,MergeSort,HeapSort慢很多。但是它相对比较简单,它适合于数据量在5000以下并且速度并不是特别重要的场合。它对于数据量较小的数列重复排序是非常好的。
void shellSort(int arr[], int len)
{
int delta,i1,i2,tmp;
for(delta = len/2; delta > 0; delta /= 2) //这里采用依次减半的步长
{
for(i1 = delta; i1 < len; i++)
{
tmp = arr[i1];
for(i2 = i1; i2 >= delta; i2 -= delta)
{
if(arr[i2-delta] > tmp)
arr[i2] = arr[i2-delta];
else
break;
}
arr[i2] = tmp;
}
}
}
//更好理解的版本
void shellSort2(int a[], int n)
{
int i, j, gap;
for (gap = n / 2; gap > 0; gap /= 2)
for (i = gap; i < n; i++)
for (j = i - gap; j >= 0 && a[j] > a[j + gap]; j -= gap)
swap(&a[j], &a[j + gap]);
}
(5) 归并排序
原理:将两个有序数组合并。这个函数在归并排序中最先执行的时候两个数组长度不超过1,所以能按从小到大的顺序排列,
然后数组长度逐渐增大,这时候参与合并的数组都已经是排好序的了
-
性能:
-
时间:O(n·log n),比堆排序稍快(可计算其算法循环执行的次数,或简单理解为归并排序只有一层循环,堆排序两层循环)
空间:O(n),也是递归的,数据量非常大时可能堆溢出
稳定性:稳定
int* merge(int a1[], int a2[], int len1, int len2)
{
if(a1 == NULL)
return a2;
if(a2 == NULL)
return a1;
int *a=(int *)malloc(sizeof(int)*(len1 + len2)); //空间:O(n),分配在堆存储区中
assert(a != NULL);
int n1 = 0, n2 = 0;
while(n1 < len1 && n2 < len2)
{
if(a1[n1] < a2[n2]) //这里决定了排序是从小到大还是从大到小
{
a[n1+n2] = a1 [n1];
n1++;
}
else
{
a[n1+n2] = a2[n2];
n2++;
}
}
while(n1 < len1)
{
a[n1+n2] = a1[n1];
n1++;
}
while(n2 < len2)
{
a[n1+n2] = a2[n2];
n2++;
}
return a;
}
void mergeStore(int a[], int startIndex, int endIndex)
{
if(startIndex >= endIndex)
return;
int middleIndex = (startIndex + endIndex) / 2; //一般选取中点作为分割点,也可以选其他的位置作为分割点
mergeStore(a, startIndex, middleIndex); //递归——第一部分排序
mergeStore(a, middleIndex+1, endIndex); //递归——第二部分排序
//两部分合起来。最先把下面第一个参数忘了加startIndex,坑死爹了
int* temp = merge(a+startIndex, a + middleIndex + 1, middleIndex - startIndex + 1, endIndex - middleIndex);
//排好序的部分放回到原来的数组
for(int i = startIndex; i <= endIndex; i++)
a[i] = temp[i-startIndex];
free(temp);
}
(6) 堆排序
原理:先大堆化数组,此时最大元素就是A[0],将数组首尾元素互换,再把除末尾元素之外的堆化,依次循环,完成排序
-
性能:
-
时间:O(n·log n),适合于数据量非常大的场合(百万数据)
空间:O(1)
稳定性:不稳定
#define LeftChild(i) (2*(i)+1) //把数组看成满二叉树按层遍历的结果,这是A[i]节点的左孩子在数组中的索引,并不是节点值
//堆化数组:把以A[i]为根节点的树调整成最大堆
void makeMaxHeap(int A[], int i, int N)
{
int child,tmp; //temp用于存储子堆中的最大节点
for(tmp = A[i]; LeftChild(i) < N; i = child)
{
child = LeftChild(i);
if(child != N-1 && A[child+1] > A[child]) //将child变成较大子节点的索引
child++;
if(A[child] > tmp) //较大子节点比父节点也大,说明该子节点就是父、左、右三节点中最大的,则需要把该子节点“上浮”(赋值给父节点)
A[i] = A[child];
else
break;
}
A[i]=tmp; //被上浮的位置用父节点来填补
}
//版本2,更好理解一点
void makeMaxHeap2(int A[], int i, int N)
{
int child;
for(; LeftChild(i) < N; i = child)
{
child = LeftChild(i);
if(A[child+1] > A[child] && child != N-1)
child++;
if(A[child] > A[i])
swap(&A[child], &A[i]);
else
break;
}
}
void HeapSort(int A[], int N)
{
int i;
//把A[]调整成大堆,构造这个堆要把下层的大的子节点上浮成父节点,所以循环的顺序只能从大到小
for(i = N/2; i >= 0; i--)
makeMaxHeap(A, i, N);
//上面构造的大堆根节点A[0]就是数组中的最大值,下面每次循环都把最大值放到后面,然后把前面剩余的数中的最大值放到A[0]的位置,从而实现从小到大排序
for(i = N-1; i > 0; i--)
{
Swap(&A[0], &A[i]);
makeMaxHeap(A, 0, i);
}
}
规律:时间复杂度为为
O(n2)
的算法都稳定。