插入排序
插入排序,顾名思义,就是以插入的方式将相应的数字插入到有序序列中的合适位置,继而产生新的有序序列。
最贴切的例子就是玩扑克牌时,会在游戏开始之前将扑克牌进行整理,手中的牌可以分成有序部分和未定部分,取出未定部分的牌插入到有序部分的合适位置,最终得到的牌便是有序的。这个过程就运用了插入排序的方法。
直接插入排序
具体步骤:
- 将待排序列分成两部分,有序序列和无序序列
- 依次将无序序列中的待插元素与有序序列的元素比较大小,移动有序序列中的元素,直到找到合适位置,放入待插元素
- 当无序序列中的元素全部插入完毕后,结束排序
图解:
给定一个 29,14,13,37,10 的序列,按升序大小排列
算法描述:
void insert_sort(int* arr,int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i; //有序末尾
int tmp = arr[end + 1]; //待插元素
while (end >= 0) //确保插入位置不越界
{
if (tmp < arr[end])
{
arr[end + 1] = arr[end]; //移动有序序列中元素
end--;
}
else //找到了合适待插位置
{
break;
}
}arr[end + 1] = tmp; //放入待插元素
}
}
算法分析:
时间复杂度:O(n2)
空间复杂度:O(1)
在最好情况下,原序列为非递减序列,可以达到只比较,不一定,时间复杂度接近O(N);
在最坏情况下,原序列为非递增序列,时间复杂度接近O(N2);
因此,对于一个接近有序的序列,使用此方法会得到事半功倍的效果。
希尔排序
希尔排序是将直接插入排序在“减少数据个数”和使“序列基本有序”两方面进行了优化。
因为不可避免的,有时候排序的序列为类似91,2,66,80,1的情况,如果直接实行直接插入排序,需要将91不断挪动,如果序列元素过多,则挪动的次数也就随之增长。
所以考虑如何将诸如此类较大元素快速挪到序列后
具体步骤:
- 将序列根据不同的增量分成相应的组,每个组内分别实行插入排序
- 减小增量,重新分组,重复步骤1
- 直到序列基本有序,增量设为1,实现直接插入排序
希尔排序的实质是——分组插入,能够减少参与插入排序的数据量。
图解:
给定一个99,49,38,65,97,13,2,100,44 的序列,按升序大小排列
第一趟下来,99一下子就能移动到后面去,2,13等也能移动到靠前的位置,当前的序列已经接近有序,接下来使用直接插入排序即可
算法描述:
void Shellsort(int* arr, int n,int gap) //每次传入不同的增量gap
{
for (int i = 0; i < n-gap; i++)
{
int end = i;
int tmp = arr[end + gap];
while (end >= 0)
{
if (tmp < arr[end])
{
arr[end + gap] = arr[end];
end-=gap;
}
else
{
break;
}
}arr[end + gap] = tmp;
}
}
交换排序
冒泡排序
冒泡排序:两两比较相邻元素,根据需要交换。这样,n个元素的序列,经过一趟冒泡排序,比较了n-1次,使得最后一个元素的位置得到确认。
同理,n个元素的序列,为使得所有元素的位置得到确认,需经过n-1趟冒泡排序,第i趟冒泡排序,比较n-i次。这个算法会重复进行,直到没有两两需要交换的元素为止。
这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
例如:对33,14,22,97,65,38,26,77,13,33 这一序列升序排序
前三趟下来,序列后三个位置确定;
以此类推,可以得到有序的序列
算法描述:
void BubbleSort(int *a, int n)
{
int flag=0; //用来标记一趟排序是否发生交换
for(int i=0;i<n;i++) //外循环为排序的趟数,n个数进行n-1趟
{
flag=0;
for(int j=0;j<n-1-i;j++) //内循环为两两比较的次数,第i趟比较n-i次
{
if(a[j+1]<a[j])
{
swap(&a[j-1],&a[j]);
flag=1;
}
}
if(flag==0) //一趟排序没有发生交换,证明序列有序
{
break;
}
}
}
快速排序
快速排序是依据一个“中值”,将序列分成两部分,小于中值的一部分和大于中值的一部分。然后,每一部分分别进行快速排序(递归实现)。通过不断确定不同“中值”的位置,实现序列排序。
具体步骤:
- 序列的第一个元素定为“中值”,附设两个指针leftmark=1、 rightmark=length-1;
2.将“中值”的位置记下,为hole
3.当leftmark<rightmark 时,向前移动rightmark,找到比“中值”小的元素位置,停下; 将该元素的值更新hole位置的值,同时更新hole=rightmark
4.向后移动leftmark,找到比“中值”大的元素位置,停下;将该元素的值更新hole位置的值,同时更新hole=leftmark
5.重复上述步骤,当leftmark==rightmark时,移动暂停,此时hole的位置是“中值”应该处于的位置,将中值填入hole中
以上为一趟快速排序
6.以中值为界,序列被分为两部分,将这两部分分别进行快速排序(递归)。
7.分裂过程递归实现,结束条件为:length<=1(序列只有一个元素)
算法描述:
void QSort(int *a,int n)
{
QuickSort(a,0,n-1);
}
void QuickSort(int *a,int first, int last)
{
if(first<last) //子序列长度大于1
{
int midvalue=Partition(a,first,last) //以“中值”为界,将子序列分为两部分,分别排序
QuickSort(a,first,midvalue-1);
QuickSort(a,midvalue+1,last);
}
}
int Partition(int *a, int first, int last)
{
int mid=a[first]; //记录“中值”大小
int leftmark=first;
int rightmark=last;
int hole=first; //记录中值位置
while(leftmark<rightmark) //从序列两端向中间遍历
{
while(leftmark<rightmark && a[rightmark]>=a[mid]) //找比“中值”小的元素的位置,停下来
{
rightmark--;
}
a[hole]=a[rightmark];
hole=rightmark;
while(leftmark<rightmark && a[leftmark]<=a[mid]) //找比“中值”大的元素的位置,停下来
{
leftmark++;
}
a[hole]=a[leftmark];
hole=leftmark;
}
a[hole]=mid; //“中值”位置确定
return hole;
}
算法分析:
如果分裂总能把序列分为相等两部分,时间复杂度O(logN)
移动是将每个元素都与中值比较,O(N)
平均时间复杂度O(NlogN)
最坏情况:当序列已经有序时,时间复杂度退化为O(N2),为避免最坏情况出现,需要改进中值的选取。采用三者取中原则,事先比较序列的第一个,最后一个,和中间的一个元素,选取居中的值作为“中值”,事先交换首元素和中值的位置。
int GetMidIndex(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[begin] < a[mid])
{
if (a[mid] < a[end])
{
return mid;
}
else if (a[begin] < a[end])
{
return end;
}
else {
return begin;
}
}
else
{
if (a[begin] < a[end])
{
return begin;
}
else if (a[mid] < a[end])
{
return end;
}
else
{
return mid;
}
}
}
利用该函数可以得到“中值”位置
此算法适用于无序序列,n比较大的情况
选择排序
简单选择排序
选择排序的基本原理:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。
具体步骤:
1.从序列中选择最小的元素,放在序列的首端,它需要与序列的首元素交换;
2.选择第二小的元素,与序列的第二个元素交换;
3.以此类推,当选择进行到序列最后一个元素时,必然是最大元素。故选择只需进行n-1次,使排序完成。
算法描述:
void SelectionSort(int *a,int n)
{
for(int i=0;i<n;i++)
{
k=i; //表示此趟排序中最小元素的下标
for(int j=i+1;j<n;j++) //找出最小元素下标
{
if(a[j]<a[k])
{
k=j;
}
}
if(k!=i) //将此趟最小元素放置已排序序列末尾
{
swap(&a[k],&a[i]);
}
}
}
算法分析:
时间复杂度O(N2)
空间复杂度O(1)
堆排序
实现堆排序首先要了解堆的特性。
大根堆——堆顶元素必为堆中元素的最大值
小根堆——堆顶元素必为堆中元素的最小值
堆排序可以分为两部分,建堆和调整堆。
建堆需要我们根据排序的要求,建立大根堆(升序)/小根堆(降序)
调整堆需要依据根叶节点关系,采用向下调整法,重新调整序列为堆。
堆排序具体步骤:
1.建堆
2.将堆顶元素与当前未经排序序列的最后一个元素交换,调整堆
3.重复2,直至全部排序完毕
算法描述:
//建堆
void creatHeap(int *a,int n)
{
for(int i=n/2;i>=0;i--)
{
HeapAdjust(a,i,n);
}
}
//向下调整
void HeapAdjust(int *a, int parent,int size)
{
int child=child1=parent*2+1;
while(child<size)
{
int child2=child1+1;
if(child2<size && a[child1]<a[child2])
{
child=child2;
}
if(a[parent]<a[child])
{
swap(&a[parent],&a[child]);
parent=child;
child=child1=parent*2+1;
}
else{
break;
}
}
}
//堆排序
void HeapSort(int *a, int n)
{
creatHeap(a,n); //建堆
for(int i=n-1;i>0;i--) //调整堆
{
swap(&a[0],&a[i]);
HeapAdjust(a,0,i);
}
}
归并排序
归并排序是分治法的一个典型应用,通过将一个序列持续分裂为两部分(递归),对这两部分进行排序合并。
递归的结束条件:序列仅有1个元素时
具体步骤:
归并的过程中,核心是将两个相邻有序序列合并为一个有序序列。
方法是,设置第三个序列,每次从给定的两个序列中分别取出一个元素,将较小者放到第三个序列中。重复此过程,直至其中一个序列为空,将剩余的元素放到第三个序列中。
算法描述:
void Merge(int *a,int *b,int low,int mid,int high)
{
//序列1的起始区间
int begin1=low;
int end1=mid;
//序列2的起始区间
int begin2=mid+1;
int end2=high;
//合并序列的长度
int k=low;
while(begin1<=end1 && begin2<=end2) //两个序列的归并
{
if(a[begin1]<a[begin2])
{
b[k++]=a[begin1++];
}
else{
b[k++]=a[begin2++];
}
}
//剩余的归并
while(begin1<=end1)
{
b[k++]=a[begin1++];
}
while(begin2<=end2)
{
b[k++]=a[begin2++];
}
}
void Msort(int*a,int low,int high,int*b)
{
if(low<high)
{
int mid=(low+high)/2;
Msort(a,low,mid,b);
Msort(a,mid+1,high,b);
Merge(a,b,low,mid,high);
}
}
void MergeSort(int *a, int n)
{
int *tmp=(int*)malloc(sizeof(int)*n);
if (tmp == NULL)
{
return;
}
else
{
Msort(a,0, n-1, tmp);
}
free(tmp);
tmp = NULL;
}
算法分析:
时间复杂度O(N*log2N)
空间复杂度O(N)