最近看了一篇好文章,结合那个的内容和一点自己的感受写了一篇给自己看。
首先排序算法分为两种,比较排序(冒泡,选择,插入,归并,堆,快排)和非比较排序(计数排序,基数排序,桶排序),总体来说比较排序的时间复杂度高一些。
这张图很好
其中要说明的是稳定和不稳定是根据算法的条件判断的。比如,冒泡排序的交换条件改为如下,则将变为不稳定排序。
A[i]>=A[i+1]
冒泡排序
由小到大的顺序排序(或由大到小) 两两比较,小的上浮,大的下沉。这个算法只需要一个辅助空间。空间复杂度为O(1)
代码如下
void swap(int A[], int i, int j,)
{
int tmp=A[i];
A[i]=A[j];
A[j]=A[tmp];
}
void BubbleSort(int A[], int n)
{
for (int j=0;j<n;j++) //j用来记录已经排好序的数的个数
{
for(int i=0;i<n-1-j;i++)//i是当前比较的数的序号
{
if(A[i]>A[i+1])
{
swap(A,i,j);//这一波操作使得每次比较后较大的数后移,未排序部分最大的数将被移到数组最后
}
}
}
}
选择排序
若是由大到小排序,将剩余的未排序数组中最小的元素挑出来放到已排序数组的末尾。空间复杂度是O(1)
void SelectionSort(int A[], int n)
{
for (int i=0;i<n;i++)//i代表已经有i-1个数排好序了,这一次要找到第i个顺序的数
{
min=i;
for (int j=i+1;j<n;j++)
{
if(A[j]<A[min])
{
min=j;
}
if(min!=i)
swap(A,min,i);//关于为什么不稳定。。看一下这个序列{5,8,5,2,9}第一轮排序将改变两个5的相对位置。。关于为什么不稳定,我想关键是否在于交换位置这一步骤是否将两个不挨着的数字进行了交换,这样这两个数字与两数中间数字的相对位置关系将发生改变,容易导致不稳定。
}
}
}
插入排序
选择排序是将剩下元素的最大值(最小)挑出来放到已排序数组的末尾,而插入排序是按顺序将未排序的元素插入已排序的数组中。空间复杂度为O(1)。这个排序算法在数量较小的情况下较好使用。且这种排序在元素接近有序的情况下接近最好情况,时间复杂度较低,达到O(n).
for (int i=1;i<n;i++)//这里是说将找第i个数字的位置
{
int j=i-1;
while(j>=0&&A[j]>A[i])//前面的数组是排好序的,这一步是找位置
{
A[j+1]=A[j];
j--;
}
if(j!=i)
A[j]=A[i]
}
希尔排序(插入排序的一种改进)
插入排序红每个元素每次只能挪动一步,而希尔排序通过改变步长,可以让元素一次挪动几步。最后一次排序是增量为1的排序,也即是插入排序,而此时数组接近有序。
这个时间复杂度不一定,根据步长的改变而改变。最好的时间复杂度是O((logn)^2),空间复杂度是O(1).
void shellsort(int A[],int n)
{
int i,j,gap;
for (gap=n/2,gap>0;gap=gap/2)//这一步是在计算步长,将步长慢慢减小到1.
{
for (i=0;i<gap;i++)//将排好序的数组放在整个数组的最前面。i代表的是将对第i组序列进行排序。
{
for (j=i+gap;j<n;j=j+gap)//对每一组序列进行插入排序。
if(A[j]<A[j-gap])
{
int tmp=A[j];
int k=j-gap;
while(k>=0&&A[k]>tmp)
{
A[k+gap]=A[k];
k=k-gap;
}
a[k+gap]=tmp;
}
}
}
}
归并排序
合并方式有点不一样?但是是稳定的,因为merge里判断那一句的等号。时间复杂度是O(nlogn),空间复杂度O(n)。
void Merge(int A[], int left, int mid, int right)// 这一步归并的是已经排序好的两个数组,left和right
{
int len=right-left+1;
int *temp=new int[len];
int index=0;
int i=left;//left数组
int j=mid+1;//right数组
while(index<len)
{
temp[index++]=A[i]<=A[j]?A[i++]:A[j++];
}
while(i<=mid)
temp[index++]=A[i++];
while(j<=right)
temp[index++]=A[j++];
for(int k=0;k<len;k++)
A[left++]=temp[k];
}
void MergeSort(int A[],int left,int right)//递归版的归并排序
{
if (left==right)
return ;
int mid=(left+right)/2;
MergeSort(A,left,mid);
MergeSort(A,mid+1,right);
merge(A,left,mid,right);
}
void MergeSort(int A[],int len)//迭代版的归并排序
{
int left,mid,right;
for (int i=1;i<len;i++)
{
for (int i=1;i<len;i*=2)//i代表每一轮排序的子数组的长度
{
left=0;
while(left+i<len)
{
mid=left+i-1;
right=mid+i<len?mid+i:len;
Merge(A,left,mid,right);
left=right+1;
}
}
}
}
归并排序很好,可以用于求数组中的逆序对和数组小和。
下面是利用merge函数求逆序对的代码
void Merge(int A[], int left, int mid, int right)
{
int len=right-left+1;
int *temp=new int[len];
int index=0;
int i=left;//left数组
int j=mid+1;//right数组
int inverseNum=0;//统计逆序对数
while(index<len)
{
if(A[i]<A[j])
temp[index++]=A[i];
else
{
inverseNum+=(mid-i+1);//第一个序列中A[i]大于A[j],而第一个序列中i后面的数都大于i,所以i后面的数都大于j
temp[index++]=A[j];
}
}
while(i<=mid)
temp[index++]=A[i++];
while(j<=right)
temp[index++]=A[j++];
for(int k=0;k<len;k++)
A[left++]=temp[k];
}
求小和的merge函数如下:
int Merge(int A[], int left, int mid, int right)
{
int len=right-left+1;
int *temp=new int[len];
int index=0;
int i=left;//left数组
int j=mid+1;//right数组
int smallSum=0;//小和
while(i<=mid&&j<=right)
{
if(A[i]<=A[j]){
smallSum+=A[i]*(right-j+1);//i<j,而j后面的数又都比j大,因此乘以数目代表加了多少遍。
temp[index++]=A[i++];
}
else
temp[index++]=A[i++];
}
while(i<=mid)
temp[index++]=A[i++];
while(j<=right)
temp[index++]=A[j++];
for(int k=0;k<len;k++)
A[left++]=temp[k];
return smallSum;
}
堆排序
用大顶堆是实现,根节点的值大于孩子节点。显然不稳定,空间复杂度O(1)。这个一般是数组存储,且近似于一个完全二叉树。
void Heapify(int A[],int i,int size)
{
int left_child = 2 * i + 1; // 左孩子索引
int right_child = 2 * i + 2; // 右孩子索引
int max = i; // 选出当前结点与其左右孩子三者之中的最大值
if (left_child < size && A[left_child] > A[max])
max = left_child;
if (right_child < size && A[right_child] > A[max])
max = right_child;
if (max != i)
{
Swap(A, i, max); // 把当前结点和它的最大(直接)子节点进行交换
Heapify(A, max, size); // 递归调用,继续从当前结点向下进行堆调整
}
}
int BuildHeap(int A[], int n) // 建堆,时间复杂度O(n)
{
int heap_size = n;
for (int i = heap_size / 2 - 1; i >= 0; i--) // 从每一个非叶结点开始向下进行堆调整
Heapify(A, i, heap_size);
return heap_size;
}
void HeapSort(int A[], int n)
{
int heap_size = BuildHeap(A, n); // 建立一个最大堆
while (heap_size > 1) // 堆(无序区)元素个数大于1,未完成排序
{
// 将堆顶元素与堆的最后一个元素互换,并从堆中去掉最后一个元素
// 此处交换操作很有可能把后面元素的稳定性打乱,所以堆排序是不稳定的排序算法
Swap(A, 0, --heap_size);
Heapify(A, 0, heap_size); // 从新的堆顶元素开始向下进行堆调整,时间复杂度O(logn)
}
快速排序
时间复杂度O(nlogn),但应该比其他时间复杂度为此的算法更快一点。空间复杂度为O(logn),最差的时候要O(n)
int Partition(int A[],int left,int right)
{
int pivot=A[right];//选择最后一个元素作为基准
int tail=left-1;
for(int i=left;i<right;i++)//小于基准的放到基准左边,大于基准的放到基准右边
{
if(A[i]<=pivot)
swap(A,++tail,i);
}
swap(A,tail+1,right);
return tail+1;//此时tail+1指向的是基准。
}
void QuickSort(int A[], int left, int right)
{
if (left >= right)
return;
int pivot_index = Partition(A, left, right); // 基准的索引
QuickSort(A, left, pivot_index - 1);
QuickSort(A, pivot_index + 1, right);
对于java的sort函数,对基础类型使用快速排序,对非基础类型使用归并排序的原因是,基础类型相同值无差别,为了更高效的排序所以选择快排,非基础类型相等实例的位置则不宜改变。此选择是根据算法的稳定性来的。
看一波非比较排序。
计数排序
是稳定的,时间复杂度与空间复杂度都是O(n+k).
const int k = 100; // 基数为100,排序[0,99]内的整数
int C[k]; // 计数数组每个元素存的是i这个数最后一次出现的位置。
void CountingSort(int A[], int n)
{
for (int i = 0; i < k; i++) // 初始化,将数组C中的元素置0(此步骤可省略,整型数组元素默认值为0)
{
C[i] = 0;
}
for (int i = 0; i < n; i++) // 使C[i]保存着等于i的元素个数
{
C[A[i]]++;
}
for (int i = 1; i < k; i++) // 使C[i]保存着小于等于i的元素个数,排序后元素i就放在第C[i]个输出位置上
{
C[i] = C[i] + C[i - 1];
}
int *B = (int *)malloc((n) * sizeof(int));// 分配临时空间,长度为n,用来暂存中间数据
for (int i = n - 1; i >= 0; i--) // 从后向前扫描保证计数排序的稳定性(重复元素相对次序不变)
{
B[--C[A[i]]] = A[i]; // 把每个元素A[i]放到它在输出数组B中的正确位置上
// 当再遇到重复元素时会被放在当前元素的前一个位置上保证计数排序的稳定性
}
for (int i = 0; i < n; i++) // 把临时空间B中的数据拷贝回A
{
A[i] = B[i];
}
free(B); // 释放临时空间
}
基数排序
稳定的,时间复杂度与空间复杂度都是O(n*dn)
这里有利用计数排序的稳定性。
const int dn = 3; // 待排序的元素为三位数及以下
const int k = 10; // 基数为10,每一位的数字都是[0,9]内的整数
int C[k];
int GetDigit(int x, int d) // 获得元素x的第d位数字
{
int radix[] = { 1, 1, 10, 100 };// 最大为三位数,所以这里只要到百位就满足了
return (x / radix[d]) % 10;
}
void CountingSort(int A[], int n, int d)// 依据元素的第d位数字,对A数组进行计数排序
{
for (int i = 0; i < k; i++)
{
C[i] = 0;
}
for (int i = 0; i < n; i++)
{
C[GetDigit(A[i], d)]++;
}
for (int i = 1; i < k; i++)
{
C[i] = C[i] + C[i - 1];
}
int *B = (int*)malloc(n * sizeof(int));
for (int i = n - 1; i >= 0; i--)
{
int dight = GetDigit(A[i], d); // 元素A[i]当前位数字为dight
B[--C[dight]] = A[i]; // 根据当前位数字,把每个元素A[i]放到它在输出数组B中的正确位置上
// 当再遇到当前位数字同为dight的元素时,会将其放在当前元素的前一个位置上保证计数排序的稳定性
}
for (int i = 0; i < n; i++)
{
A[i] = B[i];
}
free(B);
}
void LsdRadixSort(int A[], int n) // 最低位优先基数排序
{
for (int d = 1; d <= dn; d++) // 从低位到高位
CountingSort(A, n, d); // 依据第d位数字对A进行计数排序
}
还有一个桶排序。。暂时不想看了。。