冒泡排序
冒泡排序的过程就是两两比较,只要前一个比后一个大的,就进行交换,每一趟只能确定一个数的最终位置。
由于每一趟只能确定一个数的最终位置,所以需要两层循环,时间复杂度为:O(N^2),空间复杂度:O(1)。
void BubbleSort(int* a, int n)
{
for(int i = 0;i < n;i++)
{
//这里加了个小优化,如果这个数组已经是有序了,flag就为零,后面就不再运行,直接退出循环
int flag = 0;
for(int j = 1;j < n-i;j++)
{
if(a[i] > a[j])
{
flag =1;
Swap(&a[i], &a[j]);
}
}
if(flag == 0)
{
break;
}
}
}
选择排序
选择排序的思路就是在一趟循环中选出最大的,和最小的,在一趟循环结束之后,把最大的跟在数组最后一位交换,最小的跟数组第一位进行交换时间,时间复杂度为:O(N^2),空间复杂度:O(1)
// 选择排序
void SelectSort(int* a, int n)
{
//这里采用的是左闭右闭的方式进行循环
int begin = 0, end = n-1;
while(begin < end)
{
int mini = begin,maxi = begin;
for(int i = begin+1;i<=end;i++)
{
if(a[i] > a[maxi])
{
maxi = i;
}
if(a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[begin], &a[mini]);
//例如最大的数在第一位, 9 4 5 2 6 7 9 3 ,而我们先执行的是把最小的数,跟数组的第一位进行交换了,所以最大的数的位置会有变动,这就会导致我们最大的数,会变换不正确,所以我们要如下的一个判断
if(maxi == begin)
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
插入排序
插入排序的思想是,在[0,end]的区间内,插入一个数,使在[0,end+1]为有序,插入排序如果是在一个相对有序的数组内排序,他的时间是比冒泡排序,选择排序要快的多的,时间复杂度为O(n^2),空间复杂度:O(1)
// 插入排序
void InsertSort(int* a,int n)
{
for(int i = 0;i<n-1;i++)
{
//[0,end]插入end+1,在[0,end+1]为有序,
int end = i;
int tmp = a[end+1];
while(end >= 0)
{
if(a[end] > tmp)
{
a[end+1] = a[end];
end--;
}
else
{
break;
}
}
a[end+1] = tmp;
}
}
希尔排序
希尔排序是插入排序的升级版,升级的思路是把一个n个数的数组,分为gap组,即n/gap,一趟排序后,可以很快的把相对大的数,非常快的往后排,需要排序的数越多,gap的值就随着数的大而到小,如果gap为1,那就是插入排序,时间复杂度为O(n^1.5),要好于插入排序的O(n ^ 2),需要注意的是gap在变化的最后,必须为1.另外由于记录跳跃式的移动,希尔排序并不是一种稳定的排序方法
//希尔排序
void ShellSort(int* a, int n)
{
//希尔排序跟插入排序非常相似,我们把gap换成1就是插入排序
int gap = n;
while(gap>1)
{
gap = gap/3+1;
for(int i =0;i<n-gap;i++)
{
int end =i;
int tmp = a[end+gap];
while(end>=0)
{
if(a[end]>tmp)
{
a[end+gap] = a[end];
end-=gap;
}
else
{
break;
}
}
a[end+gap] = tmp;
}
}
}
一趟排序下来后就变的相对有序,在后面的几趟我们继续把gap值缩小,直到1就为有序了
堆排序
堆排序的思想跟二叉树有很大的相似,本质的思想就是要升序就建大根堆,要降序就建小根堆,大根堆的性质就是,左右子树都小于他的根,即每个根都大于他的左右子树,小根堆的性质就是,左右子树都大于他的根,即每个根都大于他的左右子树,
//交换两个数
void Swap (int* p1,int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//堆的向下调整
void AjustDown(int* a,int n,int parent)
{
int Smallchild = parent *2+1;
while(Smallchild < n )
{
//找出小的左右子树,跟根比较,
if(Smallchild+1 < n && a[Smallchild+1] > a[Smallchild])
{
Smallchild++;
}
//比根大就交换,然后根再等于子树,进行下一轮的比较,
if(a[Smallchild] > a[parent])
{
Swap(&a[parent], &a[Smallchild]);
parent = Smallchild;
Smallchild = parent*2+1;
}
else
{
break;
}
}
}
//堆排序
void HeapSort(int *a, int n)
{
//大思路:选择排序,依次选数,从后往前排
//升序:建大堆
//降序:建小堆
//建堆:向下调整堆O(N)
for(int i = (n-1-1)/2;i>=0;--i)
{
AjustDown(a,n,i);
}
int i =1;
while(i<n)
{
//每次获取堆的第一位数 再把第一位数跟最后一位数交换,下一次向下调整堆的时候 排除最后一个已经排序好的不在调整范围内
Swap(&a[0], &a[n-i]);
AjustDown(a, n-i, 0);
++i;
}
}
我们把一个数组,映射成一个二叉树,此时我们需要向下调整,以达到建成一个大根堆,向下调整的思想就是,第一次保证0~0位置大根堆结构,第二次保证0~1位置大根堆结构,第三次保证0~2位置大根堆结构...直到保证0~n-1位置大根堆结构(每次新插入的数据都与其根结点进行比较,如果插入的数比根结点大,则与根结点交换,否则一直向上交换,直到小于等于根结点,或者来到了顶端)
1.根结点下标公式:(i-1)/2(这里计算机中的除以2,省略掉小数)
2.左树下标计算公式:2*i+1
3.右树下标计算公式:2*i+2
经过堆的向下调整后,每下根节点都大于他的左右子树了,这时,我们的大根堆就建好了, 由于我们要排序的是升序,所以,我们把第一个根节点,跟下标9的节点进行交换,然后我们再把堆的范围缩小1,然后再继续调整堆
堆的每次调整,都会把最大的数,推到0下标的位置,然后我们再跟倒数第二个进行交换,再把堆调整的范围缩小-2;
快速排序
快速排序的思想是,选出一个中间值key,然后用双指针,一个从右开始往左走,一个从左往右走,把大于中间值key的排在中间值key的右边,小于中间值key的排在中间值key的左边,在经过一轮交换后,再把中间值key,跟双指针相遇的位置进行交换,这时我们的一个数的最终位置就确定了,如果我们选择的中间值是从左边选择的,需要右边指针先走,以保持右边指针跟左边指针相遇的位置是中间值key的最终排序的位置,相对的如果我们选择的是右边的值为中间值key,则左边先走,这个思想是国外大佬hoare提出来的。
//交换两个数
void Swap (int* p1,int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
//三数取中
int mid = GetMidIndex(a, left, right);
Swap(&a[right], &a[mid]);
//我们选择右边为keyi
int keyi = right;
while(left < right)
{
//由于keyi选择在右边,所以需要左边先走,以保证左跟右相撞的时候的值比keyi大
//这里是while循环 left和right会一直变动,所以需要加上判断条件,以防left错过right
//left找大
while (left < right && a[left] <= a[keyi])
{
++left;
}
//right找小
while(left < right && a[right] >= a[keyi])
{
--right;
}
if(left < right)
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
//这里需要返回keyi值,以做递归分割判断
return left;
}
//快排递归调用实现
void QuickSort(int* a, int begin, int end)
{
if(begin >= end)
{
return;
}
// 还有大佬提出在2-^(n-3)次方时采用插入排序,能进一步提升排序时间复杂度
if(end - begin <= 8)
{
InsertSort(a+begin, end-begin+1);
}
else
{
//[begin,keyi-1],keyi,[keyi+1,end];
int keyi = PartSort1(a,begin,end);
QuickSort(a,begin,keyi-1);
QuickSort(a,keyi+1,end);
}
}
这时快排的第一个思想就完成了,但有一个比较大的缺点,如果我们排的数,已经是有序了,那这个时间复杂度会很大,所以有大佬做了一个优化,我们的中间值,从左边的值跟中间的值跟右边的值,做一个判断,把这三个数中的中间值,来做中间值
//三数取中
int GetMidIndex(int* a, int left, int right)
{
int mid = (right + left)/2;
if(a[mid] > a[right])
{
if(a[mid] < a[left])
{
return mid;
}
else if(a[right] > a[left])
{
return right;
}
else
{
return left;
}
}
else //a[mid] < a[right]
{
if(a[mid] > a[left])
{
return mid;
}
else if(a[right] > a[left])
{
return left;
}
else
{
return right;
}
}
}
在加入三数取中的算法优化后,我们在遇到有序的数组时就不会那么无奈了。
快排的第二个实现方法是挖坑法,相比于hoare大佬提出的思想,进行了优先,变的写代码不那么容易出错了。
//交换两个数
void Swap (int* p1,int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
//三数取中
int mid = GetMidIndex(a, left, right);
Swap(&a[right], &a[mid]);
int key = a[right];
int hole = right;
while(left < right)
{
//找大的,填到坑
while(left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
//找小的,填到坑
while(left < right && a[right] >= key)
{
right--;
}
a[hole] = a[right];
hole = right;
}
a[hole] = key;
return hole;
}
//快排递归调用实现
void QuickSort(int* a, int begin, int end)
{
if(begin >= end)
{
return;
}
// 还有大佬提出在2-^(n-3)次方时采用插入排序,能进一步提升排序时间复杂度
if(end - begin <= 8)
{
InsertSort(a+begin, end-begin+1);
}
else
{
//[begin,keyi-1],keyi,[keyi+1,end];
int keyi = PartSort2(a,begin,end);
QuickSort(a,begin,keyi-1);
QuickSort(a,keyi+1,end);
}
}
快排的第三个实现方法是前后指针法,相比之前两个实现方法,这个显的更为简捷并且不容易出错。
//交换两个数
void Swap (int* p1,int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
//三数取中
int mid = GetMidIndex(a, left, right);
Swap(&a[left], &a[mid]);
int keyi = left;
int prev=left,cur=left+1;
while(cur <= right)
{
//在遇到比cur小的数时,并且prev++ 小于cur的时候进行交换,prev++小于cur的判断是为了两个数一样时不用进行交换
if(a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[cur], &a[prev]);
}
//cur无论如何都会往前走
++cur;
}
//最后再把keyi的值跟prev交换
Swap(&a[keyi], &a[prev]);
return prev;
}
//快排递归调用实现
void QuickSort(int* a, int begin, int end)
{
if(begin >= end)
{
return;
}
// 还有大佬提出在2-^(n-3)次方时采用插入排序,能进一步提升排序时间复杂度
if(end - begin <= 8)
{
InsertSort(a+begin, end-begin+1);
}
else
{
//[begin,keyi-1],keyi,[keyi+1,end];
int keyi = PartSort3(a,begin,end);
QuickSort(a,begin,keyi-1);
QuickSort(a,keyi+1,end);
}
}
由于快排我们前面是用递归实现的,有时数据超级大的时候,会造成栈溢出,这时我们就需要写非递归的方法实现我们的快排,非递归的快排,需要利用栈的先进后出 来实现,
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
ST st;
StackInit(&st);
StackPush(&st, left);
StackPush(&st, right);
//栈为空时,就没有数据了就跳出循环
while(!StackEmpty(&st))
{
int end = StackTop(&st);
StackPop(&st);
int begin = StackTop(&st);
StackPop(&st);
// if(begin >= end)
// {
// continue;
// }
int keyi = PartSort1(a, begin, end);
//如果左边的数已经大于右边的数了,就代表这个范围不存在没有必要入栈再拿出来
if(keyi+1 < end)
{
StackPush(&st, keyi+1);
StackPush(&st, end);
}
if(begin < keyi-1)
{
StackPush(&st, begin);
StackPush(&st, keyi-1);
}
}
StackDestory(&st);
}