经常遇到各种问题有关排序算法的,具体有:冒泡排序,交换排序,选择排序,插入排序,shell排序,快速排序,归并 排序,堆排序。当然,排序算法还有很多,桶排序,基数排序。我不懂。
先把上面所说的排序算法进行一下总结,如图(网上找的,特别好)
排序法 | 平均时间 | 最差情形 | 稳定度 | 额外空间 | 备注 |
冒泡 | O(n2) | O(n2) | 稳定 | O(1) | n小时较好 |
交换 | O(n2) | O(n2) | 不稳定 | O(1) | n小时较好 |
选择 | O(n2) | O(n2) | 不稳定 | O(1) | n小时较好 |
插入 | O(n2) | O(n2) | 稳定 | O(1) | 大部分已排序时较好 |
Shell | O(nlogn) | O(ns) 1<s<2 | 不稳定 | O(1) | s是所选分组 |
快速 | O(nlogn) | O(n2) | 不稳定 | O(nlogn) | n大时较好 |
归并 | O(nlogn) | O(nlogn) | 稳定 | O(1) | n大时较好 |
堆 | O(nlogn) | O(nlogn) | 不稳定 | O(1) | n大时较好 |
根据上面图表所述,在O(nlogn)时间上排序算法有归并和堆排序。
各种排序算法总结如下:
冒泡排序:
void BubbleSort(int data[],int n)
{
int temp = 0;
int i = 0, j = 0;
for(i = 0; i < n; ++i)
{
for (j = 0; j < n; ++j)
if(data[i] < data[j])
{
temp = data[i];
data[i] = data[j];
data[j] = temp;
}
}
}
计算时间复杂度的方法是,判断程序中重复操作的次数。根据代码可以看到重复操作的是比较,所以两个N次循环下,N^2;
对于稳定性,当给定的待排序数组中,有两个相等的数字,在排序之后不改变先后顺序的就是稳定的。
交换排序:
void swapSort(int data[],int n)
{
for (int i = 0; i < n-1; ++i)
{
for (int j = i + 1; j < n;++j)
{
if (data[i] > data[j])
{
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}
}
}
}
选择排序:
void selectSort(int data[],int n)
{
int i,j,min,t;
for (i = 0; i < n-1;++i)
{
min = i;
for (j = i + 1; j< n; ++j)
{
if (data[min] > data[j])
{ min = j;
if (min != i)
{
t = data[min];
data[min] = data[i];
data[i] = t;
}
}
for( int ttt = 0;ttt < n; ++ttt)
printf("%5d",data[ttt]);
printf("\n");
}
}
}
按照上面的代码可以看到选择排序中每一次循环过程中数组内元素变化,
插入排序:
算法描述: 插入排序把数组分成两个部分,一个部分是已经排序好的一个是还没有排序的,把没有排序的数组里面的第一个数字与已经排序好的数组进行比较插入到合适的位置。
一般情况下,都把数组的第一个元素提取出来,再从后面的数组中进行取值,与之形成一个数组,按照大小顺序进行排列。
void insertion_sort(int array[],int first,int last)
{
int i,j;
int temp;
for(i=first+1;i<=last;i++)
{
temp=array[i];
j=i-1;
//与已排序的数逐一比较,大于temp时,该数移后
while((j>=first)&&(array[j]>temp))
{
array[j+1]=array[j];
j--;
}
array[j+1]=temp;
}
}
下面的程序的方法就是不停的扩大排序数组的范围,从数组外面每次再取元素进行比较,如果在这个已经排序的数组之间,那么则插入进去。
void insertSort2(int data[],int n)
{
int i,j;
int temp ;
for (i = 1;i < n;i++)
{
temp = *(data + i);
for (j = i; j > 0 && *(data + j - 1) > temp;j--)
{
*(data+j) = *(data + j - 1);
}
*(data + j) = temp;
}
}
shell排序:
shell排序实际上是一种分组的插入排序。取一个分量d(d < n),把数组分成n份,在每个组内进行插入排序,然后,缩小d的值,
维基百科上面是是一种递减增量排序方法、并且shell排序的最主要操作是比较而不是交换,所以确定步长是最重要的部分。
void SheelSort()
{
const int n = 5;
int i, j, temp;
int gap = 0;
int a[] = {5,4,3,2,1};
int tt = 0 ;
while(gap <= n)
{
gap = gap * 3 + 1;
}
while(gap > 0)
{
printf("the %d is %d\n",++tt,gap);
for (i = gap;i < n; i++)
{
printf("in the for the %d is %d\n",tt,gap);
j = i- gap;
temp = a[i];
while((j >= 0) && (a[j] >temp))
{
a[j + gap] = a[j];
j = j - gap;
}
a[j + gap] = temp;
}
gap = (gap - 1) / 3;
}
}
可以看出上面的程序中步长的取值方法。按照维基百科上面来讲,最好的取值办法是按照下面两种方法。
已知的最好步长串行是由Sedgewick提出的 (1, 5, 19, 41, 109,...),该串行的项来自 9 * 4^i - 9 * 2^i + 1 和 4^i - 3 * 2^i + 1 这两个算式[1].这项研究也表明“比较在希尔排序中是最主要的操作,而不是交换。”用这样步长串行的希尔排序比插入排序和堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。
另一个在大数组中表现优异的步长串行是(斐波那契数列除去0和1将剩余的数以黄金分区比的两倍的幂进行运算得到的数列):(1, 9, 34, 182, 836, 4025, 19001, 90358, 428481, 2034035, 9651787, 45806244, 217378076, 1031612713, …)
另外值得说明的是在这段shell排序代码中,非常明显的可以看到出入排序的影子,正如插入排序有很多种实现方法一样,shell排序的代码也是有好多种的。快速排序:
快速排序其实上是改良的冒泡排序,步骤如下:
从要排序的数组中先挑选出一个数字称为基准,重新排列数组,把比基准大的放到基准的后面,小的放到前面。这样,就会形成以分区为界限的两部分,接下来就是递归的进行分区。
实现代码如下。
void quick_sort(int data[],size_t left,size_t right)
{
size_t p = (left + right) / 2;
int pivot = data[p];
size_t i = left, j = right;
for (;i < j ;)
{
while(!(i >= p || pivot < data[i]))
++i;
if (i < p)
{
data[p] = data [i];
p = i;
}
while(!(j <= p || data[j] < pivot))
--j;
if(j > p)
{
data[p] = data[j];
p = j;
}
}
data[p] = pivot;
if(p - left > 1)
quick_sort(data,left,p-1);
if (right - p > 1)
quick_sort(data,p+1,right);
}
进一步理解,可以把代码分成,填空和分治。先从数组中取一个值为基准(可以为第一个值,也可以取中间值),相当于这个点处为空,从数组的其他元素处开始取值进行比较,如果比这个大则放到之前基准元素的位置,此时,相当于这个大的元素的位置也是空。然后,在从后向前,取元素值与之进行比较,如果比这个元素小,那么放到之前刚被取值的那个位置。这样,进行循环比较,一次循环之后,就会形成两个数组,接下来,分治法解决。
归并排序:
归并排序是非常典型的分治法解决问题的例子。并且分治法是一种稳定的排序方法。把两个数组合并成一个有顺序的数组。
void merge_array(int*list1,int list1_size,int *list2,int list2_size);
void merge_sort(int *list,int list_size)
{
if (list_size > 1)
{
//把数组平均分成两个部分
int *list1 = list;
int list1_size = list_size / 2;
int *list2 = list + list_size / 2;
int list2_size = list_size - list1_size;
//分别归并排序
merge_sort(list1,list1_size);
merge_sort(list2,list2_size);
merge_array(list1,list1_size,list2,list2_size);
}
}
void merge_array(int*list1,int list1_size,int *list2,int list2_size)
{
int i,j,k;
i = j = k = 0;
//声明临时数组用于存储结果
int *list = (int *)malloc((list1_size + list2_size) * sizeof(int));
//只要有一个数组到达了尾部就要跳出
//也就是说只有两个都没有到达尾部的时候才执行这个循环
while(i < list1_size && j < list2_size)
{
//把较小的数字放到结果数组里面,同时移动指针
list[k++] = list1[i] < list2[j] ? list1[i++] : list2[j++];
}
//如果list1或者list2还有元素,那么是直接放到数组里面
while(i < list1_size)
{
list[k++] = list1[i++];
}
while(j < list2_size)
{
list[k++] = list2[j++];
}
//把结果数组复制到list1
for(int temp = 0; temp < (list1_size + list2_size);++temp)
{
list1[temp] = list[temp];
}
free(list);
}
这样就可以很明显的看出来了,先把一个给定的数组分成两部分,然后在归并之。
当然归并排序对于那中已经有顺序的数组是不适合的。当时如果给你两个数组你并不知道他们内部的结构的情况下,还是比较好的,占用空间,用空间换时间,快速排序当然还是比归并快。
平均时间归并还是快的。
堆排序:
堆排序是利用堆这种数据结构进行实现的。堆是一种近似完全二叉树的结构,子节点的键值总是大于或者小于它的父节点。堆节点之间的性质。i的左节点(2*i + 1)i的右节点(2*i + 2) i的父节点(i - 1)/2
根据上面的数字关系就可以用数组的形式来实现堆。
void HeadpAdjust(int *a ,int i, int size) //调整堆
{
int j, temp;
temp = a[i];
j = 2 * i + 1;
while (j < size)
{
if (j + 1 < size && a[j + 1] < a[j])
j++;
if(a[j] >= temp)
break;
a[i] = a[j]; //把较小的节点网上移动,替换他的父节点
i = j;
j = 2*i + 1;
}
a[i] = temp;
}
void BuildHeap(int *a,int size)
{
int i;
for(i = size/2 - 1;i >= 0; i--)
{
HeadpAdjust(a,i,size);
}
}
void HeapSort(int *a,int size)
{
int i;
BuildHeap(a,size);
for (i = size-1;i >= 1;i--)
{
swap(a[i],a[0]);
HeadpAdjust(a,0,i);
}
}
int main(int argc, char* argv[])
{
int a[9] = {1,2,6,3,8,3,9,5,4};
int i;
HeapSort(a,9);
for(i = 0; i < 9;i++)
cout<<a[i]<<" ";
cout<<endl;
return 0;
}
由上面的代码可以看出堆排序的思路,第一步是,建堆,按照堆的性质,每个父节点都比子节点大,或者小。必须满足这个性质,所以,堆排序就是每次把堆的头节点,取出来,此时没有了头节点的堆不满足堆的性质,所以,只能进行调整,为了便于重建,就把最后一个节点放上去,直接调整,就可以了。