排序算法是数据结构的基本算法,也是面试时面试官最喜欢提问的算法。学习排序,不仅要知道它的基本思路,还要了解它的复杂度以及稳定性。同时对于排序代码也要做到信手捏来。否则在面试时会非常尬....
排序分为以下几类:
插入排序,选择排序,堆排序,快速排序,冒泡排序,归并排序。其中插入排序又可分为俩类:直接插入排序和希尔(shell)排序。
1.插入排序:
1.1直接插入排序。
思路:在一个数组中找一个数,拿他依次和前面的值比较,如果比前面的值小,那么把前面这个数往后挪一下,把后面的挪前 来。
在此中可以找一个以end为右终点的有序区间数组,数组的个数从1开始依次增大到n-2(原数组的大小-1),然后拿end后面的值temp依次跟区间数组中的值进行比较,遇到比temp大的数,就把temp插入到这个数之前...
代码:
void InsertSort(int *a,int n)
{
assert(a);
int end = 0;
int temp = 0;
int i = 0;
for(i = 0;i < n-1;++i)
{
end = i;
temp = a[end+1]; //end后面的值赋给temp
while(end >= 0 && a[end] > temp)
{
a[end+1] = a[end]; //后移
a[end] = temp; //插入
end--;
}
}
}
从中可以看出,直接插入排序的时间复杂度是O(N*N),空间复杂度O(1)。当数组逆序时,直接插入排序达到最坏情况,这时候有一种新的插入算法叫做shell排序。
shell排序
shell排序是对数组进行预排序,使数组接近于有序。
从上述代码中可以发现,在最后调用了直接插入排序。有的童鞋可能会说shell排序明明是让算法变得更复杂,因为最后还是得进行一次直插。真的是这样么?NO!稍微回想下你就会记起,我们经常所说的时间复杂度是它最坏
情况时的复杂度,当他最坏情况不再发生时时间复杂度会小于O(N*N),当最好情况时,复杂度是O(N)。有人曾经对shell排序的时间复杂度进行计算,发现它的复杂度是O(N^1.25)~O(1.6N^1.25)。
那么shell排序比直接插入排序好是对的么?不是。shell排序不一定比直接插入排序好,当数组已经有序或者接近有序时直接插入排序更好。shell排序适用于数组反序或接近反序。
选择排序
思路:每次遍历选一个最大(或最小)的数放在数组的右边(或左边),有一种更简洁的方法:每次同时选出最大的和最小的放在它们相应的位置。
void SelectSort(int *a,int n)
{
assert(a);
for(int i = 0;i<n/2;i++)
{
int min = i;
int max = n-1-i;
for(int j = i+1;j<n;j++)
{
if(a[j] < a[min])
min = j;
if(a[n-j-1] > a[max])
max = n-j-1;
}
if(min != i) //小的可能被换到后面
{
swap(&a[min],&a[i]);
if(max == i) //max在下标为0的位置就会被换走,所以需要重新换回来
{
max = min;
}
}
if(max != n-i-1)
swap(&a[max],&a[n-1-i]);
}
}
选择排序时间复杂度是O(N*N)。
堆排序 ※
思路:排升序建大堆。每次把堆向下调整后,都会把最大的数调到堆顶,这时交换堆顶元素和最后的堆底元素,最后去掉堆底元素再进行向下调整,重复上述步骤。
代码:
void AdjustDown(int *a,int size,int root)
{
assert(a);
int parent = root;
int child = parent*2+1;
while(child < size)
{
while(child+1 < size && a[child+1] > a[child])
child++;
if(a[parent] < a[child])
{
swap(&a[parent],&a[child]);
parent = child;
child = parent*2+1;
}
else
{
break;
}
}
}
void HeapSort(int *a,int n) //升序建大堆
{
assert(a);
int i = 0;
for(i = (n-2)/2;i >= 0;i--)
{
AdjustDown(a,n,i);
}
for(i = 0;i < n;i++)
{
swap(&a[0],&a[n-1-i]);
AdjustDown(a,n-i-1,0);
}
}
快速排序:
思路:在数组中找一个基准, 使基准左边都比它小,右边都比它大,然后在俩边重复相同的步骤。
有三种方法:(此先我们先用三数取中法取到最优的中间值)
int GetMidIndex(int *a,int left,int right) //三数取中法
{
assert(a);
int mid = left + ((right - left)>>1);
if(a[left] < a[right]) //left mid right ; mid left right;left right mid
{
if(a[mid] < a[left])
return left;
else if(a[right] < a[mid])
return right;
else
return mid;
}
else
{
if(a[mid] < a[right])
return right;
else if(a[left] < a[mid])
return left;
else
return mid;
}
}
1.左右指针法:选右边的值作为中间值key,从左边找个比key大的停下,从右边找个比key小的停下。交换左右俩边的值。这样可以让左边都小右边都大。
//左右指针法
int PartSort1(int *a,int left,int right)
{
assert(a);
int mid = GetMidIndex(a,left,right);
swap(&a[mid],&a[right]);
int key = a[right];
int begin = left;
int end = right;
while(begin < end)
{
if(begin<end && a[begin] <= key)
begin++;
if(begin<end && a[end] >= key) //必须有等于
end--;
if(begin < end)
swap(&a[begin],&a[end]);
}
swap(&a[begin],&a[right]);
return begin;
}
2.挖坑法:选左右边的为坑,从左边开始找比选定的值大的,然后把这个值放在坑里,这个值原来的位置作为新的坑。然后从右边找比选定的值小的,把这个值放在坑里,一直到左右指针相等,最后把选定的值放在坑里。
//挖坑法
int PartSort2(int *a,int left,int right)
{
assert(a);
int mid = GetMidIndex(a,left,right);
swap(&a[mid],&a[right]);
int key = a[right];
int ken = right;
int begin = left;
int end = right;
while(begin < end)
{
if(begin<end && a[begin]<=key)
{
++begin;
}
else
{
a[ken] = a[begin];
ken = begin;
}
if(begin<end && a[end]>=key)
{
--end;
}
else
{
a[ken] = a[end];
ken = end;
}
}
a[ken] = key;
return ken;
}
3.前后指针法:prev刚开始赋值-1,cur赋值0.cur遇到比选定值大的值就往后走,遇到小的就停下。然后++prev再交换prev和cur的值,使小的往前走,大的往后走。prev始终指向大的值的前一个。
//前后指针法
int PartSort3(int* a,int left,int right)
{
assert(a);
int mid = GetMidIndex(a,left,right);
swap(&a[mid],&a[right]);
if(left < right)
{
int key = a[right];
int cur = left;
int prev = left-1;
while(cur < right)
{
if(a[cur] < key)
{
++prev;
if(cur != prev)
{
swap(&a[cur],&a[prev]);
}
}
++cur;
}
swap(&a[right],&a[++prev]);
return prev;
}
return left;
}
使用最终算法进行递归调用
void QuickSort(int *a,int left,int right)
{
assert(a);
int div = 0;
if(left >= right)
return;
div = PartSort3(a,left,right);
QuickSort(a,left,div-1);
QuickSort(a,div+1,right);
}
冒泡排序:
思路:前后俩个数比较,选出最大(或最小)的放在最后。但是有优化,设置一个标志,判断是否已排好序。
void BubbleSort(int *a,int n)
{
assert(a);
int i = 0;
int j = 0;
for(i = 0;i < n-1;i++)
{
int flag = 0;
for(j = 0;j < n-i-1;j++)
{
if(a[j] > a[j+1])
{
swap(&a[j],&a[j+1]);
flag = 1;
}
}
if(flag == 0)
break;
}
}
归并排序:
思路:开辟一块空间,把原来的序列每次分一半,直到只有一个值为止。然后一个一个合并。先把排序好的值铐到开辟的空间中,然后在考回原数组,区间每次增加一半,直到增回原来的值。
void _MargeSort(int *a,int left,int right,int* tmp)
{
assert(a);
if(left >= right)
return;
/*if(right-left < 20)
{
QuickSort(a,left,right);
}*/
int mid = left + ((right-left)>>1);
_MargeSort(a,left,mid,tmp);
_MargeSort(a,mid+1,right,tmp);
int begin1 = left,end1 = mid;
int begin2 = mid+1,end2 = right;
int index = left; //tmp下标
while(begin1<=end1 && begin2<=end2)
{
if(a[begin1]<=a[begin2]) //换成<=则稳定
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
while(begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
while(begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
index = left;
while(index <= right)
{
a[index] = tmp[index];
index++;
}
}
void MargeSort(int *a,int n)
{
assert(a);
int* tmp = (int*)malloc(sizeof(int)*n);
memset(tmp,0,sizeof(int)*n);
_MargeSort(a,0,n-1,tmp);
free(tmp);
}