void InsertSort(int *num, int len)
{
int i, j, temp;
for (i = 1; i < len; i++)
{
// num[i]之前的序列是有序的,num[i - 1]为有序部分的最大值
// 故只需要与有序部分的最大值进行比较,即可判断是否需要插入
if (num[i - 1] > num[i])
{
// 获取需要插入的数据
temp = num[i];
// 依次后移,查找插入位置
for ( j = i - 1; num[j] > temp && j >= 0; j--)
{
num[j + 1] = num[j];
}
// 完成插入
num[j + 1] = temp;
}
}
}
特性:元素集合越接近有序,直接插入排序算法效率的时间效率越高;最好O(N)顺序有序,最坏O(N^2)接近有序;空间复杂度O(1)。
int shsort(int s[], int n) /* 自定义函数 shsort()*/
{
int i,j,d;
d=n/2; /*确定固定增虽值*/
while(d>=1)
{
for(i=d+1;i<=n;i++) /*数组下标从d+1开始进行直接插入排序*/
{
s[0]=s[i]; /*设置监视哨*/
j=i-d; /*确定要进行比较的元素的最右边位置*/
while((j>0)&&(s[0]<s[j]))
{
s[j+d]=s[j]; /*数据右移*/
j=j-d; /*向左移d个位置V*/
}
s[j + d]=s[0]; /*在确定的位罝插入s[i]*/
}
d = d/2; /*增里变为原来的一半*/
}
return 0;
}
特性:希尔排序是对直接插入排序的优化(先分组,对分组对数据进行插入排序);当gap>1的时候都是预排序,目的是让数组更接近于有序。当gap==1时,数组已经接近有序了;时间复杂度:O(N^1.3) ~ O(N^2) 空间复杂度 : O(1)
void SelectSort(int* a, int n)
{
int left = 0, right = n - 1;
while (left < right)
{
// 选出最大的值和最小的值
int minIndex = left, maxIndex = left;
for (int i = left; i <= right; ++i)
{
if (a[i] < a[minIndex])
minIndex = i;
if (a[i] > a[maxIndex])
maxIndex = i;
}
Swap(&a[left], &a[minIndex]);
// 如果max和left位置重叠,max被换走了,要修正一下max的位置 !!!
if (left == maxIndex)
{
maxIndex = minIndex;
}
Swap(&a[right], &a[maxIndex]);
++left;
--right;
}
特性:①直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用;②时间复杂度:O(N^2) 空间复杂度 : O(1)
① 从根结点处开始,选出左右孩子中值较大的孩子下标。
② 让值较大的孩子与其父亲进行比较。
③ 如果值孩子比父亲大,将父亲的值 和 孩子的值交换 ,并将原来值较大的孩子的位置当成父亲,循环到 ① 继续向下进行调整,直到调整到叶子结点为止。如果值孩子比父亲小,则不需处理了,调整完成,整个树已经是大堆了。
堆排序的前提是有一个堆。
(1) 如何建堆? 利用向下调整算法将原数组调整为一个大堆。
2) 如何进行堆排序呢?
1、将堆顶数据与堆的最后一个数据交换,此时产生一个新的堆,不包含交换到最后位置的那一个数据,然后从堆顶位置进行一次向下调整。
2、经过步骤 1 堆中最大的数据又位于堆顶,循环执行步骤 1 ,每次把堆中的最大数据与堆的最后一个数据进行交换,以此类推就形成了一个有序的序列。
void AdjustDwon(int* a, int n, int root)
{
int parent = root;
int child = parent * 2 + 1;
while (child < n) //一直向下调整
{
if (child + 1 < n && a[child + 1] > a[child]) //child防止越界 找最大child的下标
{
child++;
}
if (a[parent] < a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else //没有比parent大的结束
{
break;
}
}
}
void HeapSort(int* a, int n)
{
//刚开始自底向上调整 建堆 - 从倒数第二层开始
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDwon(a, n, i);
}
int end = n - 1; //依次从堆顶拿到最大值放到后面
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDwon(a, end, 0);
--end;
}
}
特性分析:
①堆的向下调整算法的时间复杂度: 最坏的情况下(即一直需要交换结点),需要循环的次数为:h - 1次(h为树的高度)。而h = log2(N + 1) (N为树的总结点数)。所以堆的向下调整算法的时间复杂度为:O(logN) 。
②建堆的时间复杂度 : 假设高度为k,则从倒数第二层右边的节点开始,这一层的节点都要执行子节点比较然后交换(如果顺序是对的就不用交换);倒数第三层呢,则会选择其子节点进行比较和交换,如果没交换就可以不用再执行下去了。如果交换了,那么又要选择一支子树进行比较和交换;高层也是这样逐渐递归。
那么总的时间计算为:s = 2^( i - 1 ) * ( k - i );其中 i 表示第几层,2^( i - 1) 表示该层上有多少个元素,( k - i) 表示子树上要下调比较的次数。
S = 2^(k-2) * 1 + 2(k-3)2……+2(k-2)+2(0)*(k-1) ===> 因为叶子层不用交换,所以i从 k-1 开始到 1;
S = 2^k -k -1;又因为k为完全二叉树的深度,而log(n) =k,把此式带入;
得到:S = n - log(n) -1,所以时间复杂度为:O(n)
③排序重建堆: 在取出堆顶点放到对应位置并把原堆的最后一个节点填充到堆顶点之后,需要对堆进行重建,只需要对堆的顶点调用AdjustDown() 函数。
每次重建意味着有一个节点出堆,所以需要将堆的容量减一。AdjustDown() 函数的时间复杂度k=log(n),k为堆的层数。所以在每次重建时,随着堆的容量的减小,层数会下降,函数时间复杂度会变化。重建堆一共需要n-1次循环,每次循环的比较次数为log(i),则相加为:log2+log3+…+log(n-1)+log(n)≈log(n!)。可以证明log(n!)和nlog(n)是同阶函数:
∵(n/2)n/2≤n!≤nn,
∴n/4log(n)=n/2log(n1/2)≤n/2log(n/2)≤log(n!)≤nlog(n)
所以时间复杂度为O(nlogn)
④小结 : 初始化建堆的时间复杂度为O(n),排序重建堆的时间复杂度为nlog(n),所以总的时间复杂度为O(n+nlogn)=O(nlogn)。另外堆排序的比较次数和序列的初始状态有关,但只是在序列初始状态为堆的情况下比较次数显著减少,在序列有序或逆序的情况下比较次数不会发生明显变化。
⑤空间复杂度: O(1)
package com.kuang.array;
import java.util.Arrays;
public class ArrayDemo07 {
public static void main(String[] args) {
int[] a = {1,4,5,6,7,8,10};
System.out.println(Arrays.toString(sort(a)));
System.out.println(Arrays.toString(a));
}
//冒泡排序
//1、比较数组中,两个相邻的元素,如果第一个数比第二个数大,我们就交换他们的位置
//2、每一次比较,都会产生一个最大,或者最小的数字;
//3、下一轮则可以少一次排序!
//4、依次循环,直到结束!
public static int[] sort(int[] array){
int temp = 0;
//外层循环,判断我们这个要走多少次;
for (int i = 0;i < array.length-1;i++){
boolean flag = false;//通过flag标识位减少没有意义大比较
//内层循环,比较判断两个数,如果第一个数比第二个数大则交换他两的位置
for (int j=0;j < array.length-1-i;j++){
if (array[j+1]>array[j]){
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
flag = true;
}
}
if (flag == false){
break;
}
}
return array;
}
}
特性总结:
① 时间复杂度: O(N^2) 空间复杂度: O(1)
② 冒泡和插入相比谁更好? 顺序有序一样好,接近有序插入好
1.i =L; j = R; 将基准数挖出形成第一个坑a[i]。
2.j–由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中。
int AdjustArray(int s[], int l, int r) //返回调整后基准数的位置
{
int i = l, j = r;
int x = s[l]; //s[l]即s[i]就是第一个坑
while (i < j)
{
// 从右向左找小于x的数来填s[i]
while(i < j && s[j] >= x)
j--;
if(i < j)
{
s[i] = s[j]; //将s[j]填到s[i]中,s[j]就形成了一个新的坑
i++;
}
// 从左向右找大于或等于x的数来填s[j]
while(i < j && s[i] < x)
i++;
if(i < j)
{
s[j] = s[i]; //将s[i]填到s[j]中,s[i]就形成了一个新的坑
j--;
}
}
//退出时,i等于j。将x填到这个坑中。
s[i] = x;
return i;
}
2、分治法的代码
void quick_sort1(int s[], int l, int r)
{
if (l < r)
{
int i = AdjustArray(s, l, r);//先成挖坑填数法调整s[]
quick_sort1(s, l, i - 1); // 递归调用
quick_sort1(s, i + 1, r);
}
}
整合:
//快速排序
void quick_sort(int s[], int l, int r)
{
if (l < r)
{
//Swap(s[l], s[(l + r) / 2]); //将中间的这个数和第一个数交换 参见注1
int i = l, j = r, x = s[l];
while (i < j)
{
while(i < j && s[j] >= x) // 从右向左找第一个小于x的数
j--;
if(i < j)
s[i++] = s[j];
while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数
i++;
if(i < j)
s[j--] = s[i];
}
s[i] = x;
quick_sort(s, l, i - 1); // 递归调用
quick_sort(s, i + 1, r);
}
}
为什么要有非递归的方法?
递归 现代编译器优化很好,性能已经不是大问题
最大的问题->递归深度太深,程序本身没问题,但是栈空间不够,导致栈溢出
只能改成非递归,改成非递归有两种方式:
1、直接改循环
2、树遍历非递归和快排非递归等等,只能用Stack存储数据模拟递归过程
快速排序的非递归思路:
1、先将待排序列的左右边界的下标入栈。
2、当栈不为空时,读取栈中的数据(分别读取到一段区间的 left ,right 边界),然后调用某一版本的单趟排序,排序之后返回 key 的下标,然后判断key的左右区间是否还有数据( 数据个数 > 1),如果还有数据则将相应区间的left 和 right 边界 入栈,否则就不需要排序了。
3、循环执行执行步骤2,直到栈为空为止,此时整个区间就有序了。
void QuickSortNonR(int* a, int begin, int end)
{
stack<int> st; //辅助栈
st.push(begin); //将一个范围的左右边界入栈
st.push(end);
while (!st.empty())
{
int left, right;
right = st.top(); //获取左右边界
st.pop();
left = st.top();
st.pop();
int keyi = PartSort1(a, left, right); //对这个区间进行单趟排序
if (left < keyi - 1) //将排序完的数据再进行划分,划分为左右两个区间
{ //将左右两个区间的 ,左右边界分别入栈
st.push(left);
st.push(keyi - 1);
}
if (keyi + 1 < right)
{
st.push(keyi + 1);
st.push(right);
}
}
}
最理想的情况下:如果每趟排序所选的key正好是该序列有序时的中间值,那么一趟排序之后key就位于序列正中间,此时的快速排序的时间复杂度就是O(NlogN)。
最差的情况下:当待排序列本就是一个有序的序列 或者 接近有序时,如果仍然选择最左边或者最右边的数作为key,那么快速排序的效率将达到最低 ,O(N^2).
为了防止这种极端情况对我们效率的影响,于是出现了三数取中来进行优化:
三数取中指的是:最左边的数、最右边的数以及中间位置的数,我们取中三个数的中间值做key, 将这个值放到最左边 或者 最右边,这就确保了我们所选取的数不会是序列中的最大或是最小值了。
int GetMidIndex(int* a, int left, int right)
{
int mid = (left + right) >> 1;
// left mid right
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else // a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
② 为了减少递归树的最后几层递归,我们可以设置一个判断语句,当序列的长度小于某个数的时候就不再进行快速排序,转而使用其他种类的排序。小区间优化若是使用得当的话,会在一定程度上加快快速排序的效率,而且待排序列的长度越长,该效果越明显。
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
// 1、如果这个子区间是数据较多,继续选key单趟,分割子区间分治递归
// 2、如果这个子区间是数据较小,再去分治递归不太划算
if ((end - begin) > 10) //个数 > 10 使用递归 一般给 10 ~ 20,现在编译器对递归的优化已经很好了
{
int keyi = PartSort3(a, begin, end);
//[begin ,keyi - 1] keyi [keyi+1 , end] 子区间 //相当于前序
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
else //个数 <= 10 再使用递归消耗函数栈帧比较大
{
InsertSort(a + begin, end - begin + 1); //这里可以灵活修改,针对某个场景进行优化
}
}
归并排序就是不断的将两组有序的序列进行合并(第一次将单独的一个元素当作一组有序序列),从而得到最终的排序结果。所以首先给出如下一段代码,用来合并两组有序的序列(排升序):
void MergeData(int* arr,int left,int mid,int right,int* s){//对两组有序序列进行归并
int begin1=left;//第一组序列的首元素下标
int end1=mid-1;//第一组序列最后元素下标
int begin2=mid;//第二组序列首元素下标
int end2=right-1;//第二组序列最后元素下标
int count=left;//进行归并时第一个元素应放在对应的位置
while(begin1<=end1&&begin2<=end2){//归并两组序列
if(arr[begin1]<=arr[begin2]){
s[count]=arr[begin1];
begin1++;
}else{
s[count]=arr[begin2];
begin2++;
}
count++;
}
while(begin1<=end1){//如果第一组序列有剩余,继续归并
s[count]=arr[begin1];
count++;
begin1++;
}
while(begin2<=end2){//如果di二组序列有剩余,继续归并
s[count]=arr[begin2];
count++;
begin2++;
}
}
以上代码中,因为合并时数字都在同一数组当中,所以它们的下标是连续的,在合并时需要使用新的空间来放合并后的结果,注意:合并后的结果应该放在新申请出的空间的对应位置,即count必须从left开始,因为在以下代码中需要拷贝。
void MergeSort(int* arr,int left,int right,int* s){//归并排序递归
if(right-left<=1){//元素个数小于1,直接返回
return;
}
int mid=left+(right-left)/2;
MergeSort(arr,left,mid,s);//归并左半部分
MergeSort(arr,mid,right,s);//归并右半部分
MergeData(arr,left,mid,right,s);//将当前有序的序列进行归并
memcpy(arr+left,s+left,sizeof(arr[0])*(right-left));//将归并后的将结果拷贝回原来的数组,为下次归并做准备
}
上述递归过程中不断将问题的规模缩小,直到对两个元素进行排序,而后在层层处理其余部分。归并排序的递归格式有点像二叉树的后序遍历,很方便记忆。注意:每次归并完一组数据,都要把归并好的结果拷贝回原来的空间,还要注意当前拷贝的位置与拷贝的元素个数。
图片中的例子只是一种理想的情况,在这里我们还会遇到三种需要特殊处理的情况:
void MergeSortNonR(int* arr,int size){
int* s=(int*)malloc(sizeof(arr[0])*size);
int gap=1;//相当于第一次每一个为一组
while(gap<size){
int i=0;
for(;i<size;i+=2*gap){
int left=i;
int mid=left+gap;
if(mid>size){//越界,进行复位
mid=size;
}
int right=mid+gap;
if(right>size){
right=size;
}
MergeData(arr,left,mid,right,s);
}
gap*=2;
memcpy(arr,s,sizeof(arr[0])*size);//拷贝
}
}
当gap=1时,此时每两个元素一组,把这两个元素进行了排序,然后不断将数组元素划分为两个一组,全部排序。到下一次gap就会增长2倍,此时四个元素一组,重复上述过程。直到gap>size,说明整个归并排序结束。注意:此时的拷贝只需要在gap变化之后进行整体拷贝。
特性总结:
**1.时间复杂度:O(N*logN) **
排序的过程像是一颗二叉树,每一层表示归并这层元素,复杂度为O(N),深度为logN,所以整体的时间复杂度为:O(NlogN)
2、空间复杂度:O(N)
每次在堆上申请的空间大小与原数组相同,所以空间复杂度为:O(N)
2.归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题
#include
#include
#include
void MergeData(int* arr,int left,int mid,int right,int* s){//对两组有序序列进行归并
int begin1=left;//第一组序列的首元素下标
int end1=mid-1;//第一组序列最后元素下标
int begin2=mid;//第二组序列首元素下标
int end2=right-1;//第二组序列最后元素下标
int count=left;//进行归并时第一个元素应放在对应的位置
while(begin1<=end1&&begin2<=end2){//归并两组序列
if(arr[begin1]<=arr[begin2]){
s[count]=arr[begin1];
begin1++;
}else{
s[count]=arr[begin2];
begin2++;
}
count++;
}
while(begin1<=end1){//如果第一组序列有剩余,继续归并
s[count]=arr[begin1];
count++;
begin1++;
}
while(begin2<=end2){//如果di二组序列有剩余,继续归并
s[count]=arr[begin2];
count++;
begin2++;
}
}
void MergeSort(int* arr,int left,int right,int* s){//归并排序递归
if(right-left<=1){//元素个数小于1,直接返回
return;
}
int mid=left+(right-left)/2;
MergeSort(arr,left,mid,s);//归并左半部分
MergeSort(arr,mid,right,s);//归并右半部分
MergeData(arr,left,mid,right,s);//将当前有序的序列进行归并
memcpy(arr+left,s+left,sizeof(arr[0])*(right-left));//将归并后的将结果拷贝回原来的数组,为下次归并做准备
}
void MergeSortNonR(int* arr,int size){
int* s=(int*)malloc(sizeof(arr[0])*size);
int gap=1;//相当于第一次每一个为一组
while(gap<size){
int i=0;
for(;i<size;i+=2*gap){
int left=i;
int mid=left+gap;
if(mid>size){//越界,进行复位
mid=size;
}
int right=mid+gap;
if(right>size){
right=size;
}
MergeData(arr,left,mid,right,s);
}
gap*=2;
memcpy(arr,s,sizeof(arr[0])*size);//拷贝
}
}
void Print(int* arr,int size){
int i=0;
for(;i<size;i++){
printf("%d ",arr[i]);
}
printf("\n");
}
int main(){
int arr[]={5,7,1,3,9,8,0,2,4,6};
//int* s=(int*)malloc(sizeof(arr));
//MergeSort(arr,0,sizeof(arr)/sizeof(arr[0]),s);
MergeSortNonR(arr,sizeof(arr)/sizeof(arr[0]));
Print(arr,sizeof(arr)/sizeof(arr[0]));
return 0;
}