思路:从单趟的角度来看,就是在一个已经有序的数组内,插入一个随机的数,先将该数放到数组的最后一位,然后再往前比较,若该数字较小(默认升序),则进行交换调整,调整到有序,整体的角度就是将一堆无序的数,从第一个数开始,不断将后面的数视为插入的数,进行调整到有序,直到将所有数都遍历一遍。
分析:在遇到一些相对有序或者接近有序的数据时,排序速度较快,但当遇到逆序时,排序速度最慢
void InsertSort(int* a, int n)
{
int i;
for(i=0;i= 0)
{
if(a[end] > tmp)
{
a[end+1] = a[end] ;
end--;
}
else
{
break;
}
}
a[end+1] = tmp;
}
}
思路:希尔排序是在直接插入排序上进行改造,在直接插入排序前,先进行预排序,使得数据相对有序,或者说让大数据先尽可能快的靠后放,小数据尽可能快的靠前(默认排升序)。
预排序:选择一个合适的步长gap,将数据分成gap组,对每组进行直接插入排序,可以将小的数据尽可能往前放,大数据往后放,预排可以进行多次
我们可以将几组数据同时进行排序,只要控制好步长即可
void ShellSort(int* a, int n)
{
int gap = n/2;
int i;
while(gap > 1)//当gap为1时,则为对整体进行直接插入排序
{
gap=gap/2;
for(i=0;i= 0)
{
if(a[end] > tmp)
{
a[end+gap] = a[end] ;
end-=gap;
}
else
{
break;
}
}
a[end+gap] = tmp;
}
}
}
思路:从两边向中心进行压缩,对中间数据进行扫描,选出最小的和最大的放在两侧,不断缩小范围。
//思路:在左右各选一个下标,对中间区域进行扫描,选出最小的和最大的放在两侧,不断向中心压缩
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n-1;
while(begin a[i])
{
mini = i;
}
if(a[maxi] < a[i])
{
maxi = i;
}
}
sawq(&a[begin],&a[mini]);
if(maxi == begin)
{
maxi == mini;
}
sawq(&a[end],&a[maxi]);
begin++;
end--;
}
}
思路:
首先得知道堆是什么,堆的本质是一个用顺序表构建的二叉树,要满足子节点都小于父节点(大堆),其中涉及到向下调整的算法,以及建堆算法
向下调整算法,在一个已经建好的堆中要删除堆顶数据时,将堆顶的数与堆最末尾的数进行交换,然后再与原本堆顶的两个子节点进行比较,不断交换调整,最终重新成为一个堆,末尾的最大值作为无效数据
堆排序,就是利用向下调整算法的思路,将末尾的最大值作为有效数据,而每次在堆中选出一个最大值往后放,最终将堆内所有数,从后往前,从大到小的进行排序,也就是堆排序
建堆算法,是用于快速将一组数据建成堆的算法,同样是利用向下调整算法,从最后节点的父节点开始逐一往上继续向下调整,直到到达堆顶的位置结束。
由于前面有详细整理过堆的内容,这里就只是简略的回顾一下基本思路
从零开始实现堆排序,首先要先有向下调整算法HeapDown
void HeapDown(int* a,int n,int parent)
{
while(parent= n)
{
break;
}
//当只有左孩子时
else if(Rchild == n)
{
if(a[parent] < a[Lchild])
sawq(&a[parent],&a[Lchild]);
//该情况调整结束后,可以直接跳出
break;
}
//当左右孩子都有时
else
{
int child = a[Lchild] > a[Rchild] ? Lchild : Rchild;
if(a[parent] < a[child])
sawq(&a[parent],&a[child]);
//该情况结束单次调整后,继续向下调整
parent = child;
}
}
}
堆排序的思路就是利用向下调整算法将大的数不断往后放,因此升序用大堆,降序用小堆
void HeapSort(int* a,int n)
{
//首先需要先建堆
int i;
for(i=(n-1-1)/2;i>=0;i--)
{
HeapDown(a,n,i);
}
//堆排序
while(n)
{
sawq(&a[0],&a[n-1]);
n--;
HeapDown(a,n,0);
}
}
思路:冒泡排序就是不断的把大的数往后放,从第一个开始扫描,相邻两个进行比较,大的往后走
每次至少能将最大的放到最后面,可以做个小小的优化,在单趟排序中,若一次都没有进行交换,说明数据已经有序,则可以跳出循环
void BubbleSort(int* a, int n)
{
int i,j;
for(i=0;i a[j+1])
{
sawq(&a[j],&a[j+1]);
flag = 1;
}
}
if(flag)
break;
}
}
思路:单趟快排的思路,就是选出一个较为考中的数作为key,通过单趟快排找到key的正确位置,并且以key为分界点,同时要保证key的前半部分都比key小,key的后半部分都比key大,我们可以将其写为一个函数,最终返回keyi
具体实现单趟快排的思路有三种:
霍尔最初的版本:
选取左边为key(任意一边),然后一左一右两边往中间走,如果选择左边为key,则右边先走,右边找比key小的数字,找到后停下,然后左边走,左边找比key大的数,找到后也停下,此时,左右两边进行交换,不断重复该过程,最终左右下标相遇的点,即为key正确的位置,将key与该位置交换,此时比key小的都被换到key的前面,比key大的都换到了key后面,然后返回该位置下标。
//两边走
//两边往中走,左边的负责找大,右边负责找小,然后交换,最终相遇点既是分界点
int hoare1(int* a,int left,int right)
{
int keyi = left;//可采用三数取中优化
while(left < right)//相遇后跳出
{
while(left < right && a[right] >= a[keyi])//右边找小
right--;
while(left < right && a[left] <= a[keyi])//左边找大
left++;
//找到后交换
swap(&a[left],&a[right]);
}
swap(&a[keyi],&a[left]);
keyi = left;
return keyi;
}
挖坑法:
挖坑法的基本思路和最初版本相似,想将key的值挖出记录,也是选择最左边(左右边都可),同样左右边各有一个下标,挖出左边则右边下标先走,找比key值小的值,找到后填入坑中,该位置替代其成为新的坑,左边再开始走,找大于key的数填到坑中,不断重复,左右边相遇时,此时的位置就是key的正确位置
//挖坑法
//将key“挖”出,其位置作为坑位,右边找小挖出后将原坑填上,形成新的坑,左边再找大填上,最终相遇坑点即为key的落脚点
int hoare2(int* a,int left,int right)
{
int key = a[left];//可采用三数取中优化
int hole = left;
while(left < right)//相遇后跳出
{
while(left < right && a[right] >= key)//右边找小
right--;
swap(&a[hole],&a[right]);
hole = right;
while(left < right && a[left] <= key)//左边找大
left++;
swap(&a[hole],&a[left]);
hole = left;
}
a[hole] = key;
return hole;
}
快慢指针法:
该方法采用两个指针,两个都开始与起始点,由快指针进行向前扫描,遇到比key小的值,则慢指针走一步再与快指针的数值交换,随后快指针继续向前扫描,直到快指针将数据扫描完,慢指针就相当于一个界线,快指针找到小于该界线的都往界限前放,最终,快指针扫描完后,慢指针的位置就是key的正确位置,再将key与该位置交换,将该位置返回,即可实现单趟快排的要求。
//快慢指针法
//将两个指针一前一后一起走,慢指针作为分界线,快指针负责找小,快指针遍历一遍,将所有的小放到慢指针后面的位置
//最后慢指针停住的位置即是分界点,将key交换到此处
int hoare3(int* a,int left,int right)
{
int slow = left;
int fast = left;
int keyi = left;
while(fast <= right)
{
if(a[fast] < a[keyi])
{
slow++;
swap(&a[slow],&a[fast]);
}
fast++;
}
swap(&a[keyi],&a[slow]);
return slow;
}
三种不同的方法,都是为了将key放到排序完成后的位置,然后需要以该位置进行划分,因此返回该位置的下标,而接下来的思路,就是将划分后的区间(前半部分和后半部分)不断的进行单趟快排,将区间不断划分,最终完成整体的排序
思路:递归快排就直接将单趟快排结束后得到的界限,直接划分好前半段和后半段递归下去,当区间只有1或者小于1时,则返回。
void QuickSort(int* a,int begin,int end)
{
int keyi;
//递归停止条件
if(begin >= end)
return;
keyi = hoare3(a,begin,end);
QuickSort(a,begin,keyi-1);
QuickSort(a,keyi+1,end);
}
思路:递归无非就是通过建立栈帧的方式,能够记录多个区间不断分化下去,采用非递归实现的关键就是,每次单趟过后,能够将区间记录起来,因此我们借用栈去记录每次单趟结束后左右两边的区间,然后每组取出继续进行单趟快排,在区间大于1的时候(left < right),才需要记录前后区间去进行排序,小于等于时不记录,当栈内为空时,就说明所有被分化的区间都已经进行排序完成,也就实现了整体的排序。
void QuickSort2(int* a,int begin,int end)
{
//创建栈,将第一组区间先放入栈中
ST p;
STInit(&p);
STPush(&p,begin);
STPush(&p,end);
while(!STEmpty(&p))
{
int left,right,keyi;
right = STTop(&p);
STPop(&p);
left = STTop(&p);
STPop(&p);
keyi = hoare3(a,left,right);
if(keyi+1 < right)
{
STPush(&p,keyi+1);
STPush(&p,right);
}
if(left < keyi-1)
{
STPush(&p,left);
STPush(&p,keyi-1);
}
}
STDestory(&p);
}
关于开排的优化:
三数取中(也可以随机取):
根据快排的思路,当选出的key能够尽可能的落在整体的中间位置时,排序效率会较高,因此在选择key时,可以在最左边、中间、最右边这三个数之间选靠中间的数,只需要选出后,将这个数与最左边的位置交换即可,还是选择最左边的数为key
//优化 三数取中
int GetMid(int* a,int begin,int end)
{
int mid = (begin+end)/2;
if(a[begin] > a[mid] && a[begin] > a[end])
{
if(a[mid] > a[end])
return mid;
else
return end;
}
else if(a[mid] > a[begin] && a[mid] > a[end])
{
if(a[begin] > a[end])
return begin;
else
return end;
}
else
{
if(a[begin] > a[mid])
return begin;
else
return mid;
}
}
小区域优化:
这个优化是针对递归快排的优化,递归需要建立大量的栈帧,而实际上在数据量较小时,使用快排的优势并不明显,当区域已经分化到15个数字左右时,无论哪种排序消耗都相差不大,而快排反而需要建立大量的栈帧去完成最后数字不多时的排序,因此,可以在分化到数据量较小时,选择直接插入排序去将每个区域的数组完成排序,节省了大量栈帧创建的消耗。
void QuickSort(int* a,int begin,int end)
{
if(end - begin < 15)//若区间小于15左右时,使用插入排序即可
{
InsertSort(a+begin,end-begin+1);
return;
}
else//快排
{
int keyi;
//递归停止条件
if(begin >= end)
return;
keyi = hoare3(a,begin,end);
QuickSort(a,begin,keyi-1);
QuickSort(a,keyi+1,end);
}
}
三路排:
三路排的思想是针对于数据中有大量重复的数或者所有数完全相同时的极端情况,它采用三个下标进行控制,分别为left、cur、right。
left和right分别在左右两边,cur在left的下一位,由cur进行遍历扫描:
当cur遇到小于key的数字时:往前丢,即交换left和cur所在的数值,然后同时往前走(left++,cur++);
当cur遇到等于key的数字时:cur继续往前走(cur++);
当cur遇到大于key的数字时:cur所在的数值与right数值进行交换,然后right往中间走,cur不动(right--);
直到cur超过了right时,整个数组会被分为三部分,前半部分都是小于key的数,中间部分都是与key相同的数,后半部分全是大于key的数;
void QuickSort(int* a,int begin,int end)//三路快排
{
//小区间优化
if(end - begin < 15)//若区间小于15左右时,使用插入排序即可
{
InsertSort(a+begin,end-begin+1);
return;
}
else//三路快排
{
int left = begin;
int right = end;
int key = a[begin];
int cur = begin+1;
//递归停止条件
if(begin >= end)
return;
//三路快排
while(cur <= right)
{
//当cur遇到小于key值时
if(a[cur] < key)
{
swap(&a[left++],&a[cur++]);//交换到前面,left和cur一起走
}
else if(a[cur] == key)//等于key时
{
cur++;
}
else//大于key时
{
swap(&a[right--],&a[cur]);//交换到后面,right往中间靠
}
}
QuickSort(a,begin,left-1);
QuickSort(a,right+1,end);
}
思路:归并就是将两个有序的数组合并成一个有序的数组,利用后序的递归,从中间分,假定左右两边都各自有序,则可以直接进行归并,先将归并的代码写出,确定好两个数组各自的范围,然后进行归并,归并的过程在临时数组中进行,归并结束后再将临时数组的数拷贝回去原数组,然后就是后序递归,结束条件是当递归数组内只有一个数时,即可认为有序。
//递归归并
//思路:后序递归,将两部分有序的数组比较归并到一个数组中,其中要额外开辟一个数组
void _MergeSort1(int* a,int begin,int end,int* tmp)
{
int mid = (begin+end)/2;
int begin1,end1,begin2,end2,i;
//递归结束条件
if(begin >=end)
return;
//划分两个区间
begin1 = begin; end1 = mid;
begin2 = mid+1; end2 = end;
i = begin;
//递归两个区域,使得两区域都满足有序
_MergeSort1(a,begin1,end1,tmp);
_MergeSort1(a,begin2,end2,tmp);
//归并
while(begin1 <= end1 && begin2 <= end2)
{
if(a[begin1] <= a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while(begin1 <= end1)//若前半部分还有剩下的,则直接接到后序
{
tmp[i++] = a[begin1++];
}
while(begin2 <= end2)//若后半部分还有剩下的,则直接接到后序
{
tmp[i++] = a[begin2++];
}
//最后将临时数组里的数字复制到原数组中
memcpy(a+begin,tmp+begin,sizeof(int)*(end-begin+1));
}
void MergeSort(int* a,int n)
{
int *tmp = (int*)malloc(sizeof(int)*n);
if(tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
//归并
_MergeSort(a,0,n-1,tmp);
free(tmp);
tmp = NULL;
}
思路:非递归法则是用了一种正向思考的方法,将一组数据从左到右,分组不断进行归并,第一次每组内一个数,对整个数组进行归并后,数组内的数两两有序,此时再两个一组,两两归并,归并结束后每四个组有序,以此类推,直到所有数有序,此时就需要一个gap来控制每组的个数,每完成一次整体分组归并后就gap*2,直到gap大于数据个数
而要实现上述思路,要注意:
1.单次整体分组归并时,每组的边界要给明白
2.每次整体分组归并结束后,需要将在临时数组内归并的结果,复制回原数组,此时要注意一个是范围,一个是复制回去的数据大小不可以是2*gap,而是end2-i+1,这个部分情况下可能不等(越界时,区间修正导致end2被修改时),也可以完全完成归并后复制回
3.当两两归并到最后两组时,可能存在着越界的问题,因此需要分类讨论结合画图去修正区间。
//用一个gap代表每组要归并的个数,从左往右归并,走循环,同时控制好边界
void _MergeSort2(int* a,int begin,int end,int* tmp)
{
int gap = 1;
int n = end-begin+1;
while(gap < n)
{
int i,begin1,end1,begin2,end2;
for(i=0;i= n)
break;
if(begin2 >= n)
break;
if(end2 >= n)
{
end2 = n-1;
}
//归并
while(begin1 <= end1 && begin2 <= end2)
{
if(a[begin1] <= a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while(begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while(begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
memcpy(a+i,tmp+i,sizeof(int)*(end2-i+1));
}
gap*=2;
}
}
稳定性:稳定性指的是,相同的数字在排序后,相对位置不变,则可以说该排序稳定性好
要求能够充分理解下图各大排序的稳定性,能够通过分析思想去判断时间、空间复杂度以及稳定性
直接插入排序:
直接插入排序的思想就是从第二个开始,往前扫描,将数字通过比较插入到合适的位置,只要在遇到相等时插入到其后面,则能够满足稳定性
希尔排序:
希尔排序在预排序的时候,会将相对顺序破坏
选择排序:
选择排序在交换的时候,可能会将相同的数字通过交换,改变相对顺序,因此不稳定
堆排序:
堆排序在交换的过程中,某个节点的左右两个子节点是相同数字时,此时若是左子节点向下调整则会改变相对顺序
冒泡排序:
只要在遇到相同数字时,不进行交换即可
快速排序:
相同的数字可能会被作为key放到中间,会破坏相对位置
归并排序(升序):
从左到右,只要遇到相等的数时,将其左边的先放到临时数组即可不破坏相对位置
本篇是对排序该章节学习后的一个总结,归纳整理了思路以及代码,方便以后复习。