赞
踩
冒泡排序应该是最简单也是最容易理解的一种排序算法,它的逻辑是每趟将最大的数移到数组的最右边:
我之前写过冒泡排序的详细说明,链接如下:冒泡排序
代码实现:
//交换函数 void Swap(int* a, int* b) { int tmp = *a; *a = *b; *b = tmp; } void BubbleSort(int* a, int n) { int i,j; for (i = 0; i < n - 1; i++)//趟数 { int exchange = 0;//如果没有发生排序说明已经有序,exchange为0 for (j = 0; j < n - i - 1; j++)//交换的次数 { if (a[j] > a[j + 1])//交换两个元素 { Swap(&a[j], &a[j + 1]); exchange = 1;//第一趟发生了排序 } } if (exchange == 0)//如果没发生排序直接跳出循环 { break; } } }
最好的情况:排序前的数组已经是有序的,则只需要进行n-1次比较即可,没有数据交换,时间复杂度为O(N)。
最坏的情况:排序前数组,则需要比较并交换n-1+n-2+…3+2+1次,也就是n(n-1)/2次 时间复杂度为O(N2)。
时间复杂度是最坏的情况,O(N2)
空间复杂度是O(1).
快排的性能在所有排序算法里面是最好的,数据规模越大快速排序的性能越优。快排在极端情况下会退化成 O(N2)的算法,因此假如在提前得知处理数据可能会出现极端情况的前提下,可以选择使用较为稳定的归并排序。
快速排序算法通过多次比较和交换来实现排序,其排序流程如下:
快速排序的比较和交换方式可以通过递归和非递归的形式完成,每种形式又可以通过三种方式完成:
挖坑法的实现思路是:
这样一趟排序让6的左边都是比它小的数,右边都是比它大的数,如果再将6的左区间和右区间用相同的方法递归下去,则可以实现最终的排序。
代码实现:
//挖坑法 void QuickSort1(int* a, int left, int right)//left是数组a的左下标,right是右下标 { if (left >= right)//当只有一个数据或是序列不存在时,不需要进行操作 { return; } int begin = left, end = right;//确定begin和end的位置 int pivot = begin;//选最左边左边为坑 int key = a[begin];//选最左边的元素为key while (begin < end)//begin和end相遇时停止 { //右边找小,放到左边 while (begin < end && a[end] >= key)//遇到大于等于key的元素就继续向右走 { end--; } //小的放到坑位,自己形成新的坑位 a[pivot] = a[end]; pivot = end; //左边找大,自己形成新的坑 while (begin < end && a[begin] <= key)//遇到比小于等于key的元素就继续往左走 { begin++; } //大的放到坑位,自己形成新的坑位 a[pivot] = a[begin]; pivot = begin; } a[pivot] = key;//begin和right相遇后跳出循环,将key放入相遇的坑位 QuickSort1(a, left,pivot-1);//递归左区间 QuickSort1(a, pivot+1,right);//递归右区间 }
左右指针法和挖坑法差不多,唯一的区别在于左右指针法是交换两个数,挖坑法是将数放入坑里:
另外,这种方法和挖坑法排完一趟后数的顺序是不一样的。
再将6的左区间和右区间用相同的方法递归下去,则可以实现最终的排序。
//左右指针快排 void QuickSort2(int* a, int left, int right) { if (left >= right)//当只有一个数据或是序列不存在时,不需要进行操作 { return; } int begin = left, end = right;//确定begin和end的位置 int key = begin;//key为最左边的元素 while (begin < end) { //找小 while (begin < end && a[end] >= a[key]) { end--; } //找大 while (begin < end && a[begin] <= a[key]) { begin++; } Swap(&a[begin], &a[end]);//交换 } Swap(&a[begin], &a[key]);//相遇时和key交换 QuickSort1(a, left, begin - 1);//key的左序列进递归 QuickSort1(a, begin + 1, right);//key的右序列进行递归 }
前后指针法的步骤:
再将6的左区间和右区间用相同的方法递归下去,则可以实现最终的排序。
//前后指针快排 void QuickSort3(int* a, int left, int right) { if (left >= right)//当只有一个数据或是序列不存在时,不需要进行操作 { return; } int key = left; int prev = left, cur = left + 1; while (cur <= right)//当cur>right也就是越界时,跳出循环 { if (a[cur] < a[key] && ++prev != cur) { Swap(&a[prev], &a[cur]);//交换 } cur++; } Swap(&a[key], &a[prev]);//交换key和prev指针指向的内容 QuickSort3(a, left, prev - 1);//key的左序列进行递归 QuickSort3(a, prev + 1, right);//key的右序列进行递归 }
如果使用非递归实现,也就意味着我们需要使用额外的操作来起到递归的效果,一般来说可以用循环或者栈模拟递归的过程,这里只能使用栈来实现。
既然要用非递归,那么我们原来的递归函数就要修改成非递归只能排一趟了,但具体的思路没有变,只是多了一个返回值:
//挖坑法 int PartSort1(int* a, int left, int right) { if (left >= right)//当只有一个数据或是序列不存在时,不需要进行操作 { return; } int begin = left, end = right; int pivot = begin; int key = a[begin]; while (begin < end) { //右边找小,放到左边 while (begin < end && a[end] >= key) { end--; } //小的放到坑位,自己形成新的坑位 a[pivot] = a[end]; pivot = end; //左边找大,自己形成新的坑 while (begin < end && a[begin] <= key) { begin++; } a[pivot] = a[begin]; pivot = begin; } a[pivot] = key; return pivot; }
//左右指针法 int PartSort2(int* a, int left, int right) { int begin = left, end = right; int key = begin; while (begin < end) { //找小 while (begin < end && a[end] >= a[key]) { end--; } //找大 while (begin < end && a[begin]<= a[key]) { begin++; } Swap(&a[begin], &a[end]); } Swap(&a[begin], &a[key]); return begin; }
//前后指针法 int PartSort3(int* a, int left, int right) { int index = GetMidIndex(a, left, right); Swap(&a[left], &a[index]); int key = left; int prev = left, cur = left + 1; while (cur <= right) { if (a[cur] < a[key]&&++prev!=cur) { Swap(&a[prev], &a[cur]); } cur++; } Swap(&a[key], &a[prev]); return prev; }
利用栈先进后出的特点,用栈实现非递归的思路:
在这里需要用到栈的函数,在之前的文章中有线性表之栈和队列
void QuickSortNonR(int* a, int n) { ST st;//创建一个栈 StackInit(&st);//初始化栈 //栈里的区间就是需要被单趟分割排序的 StackPush(&st, n - 1);//将最后一个元素下标入栈 StackPush(&st, 0);//将第一个元素下标入栈 while (!StackEmpty(&st))//栈不为空则执行循环 { int left = StackTop(&st);//获取左边第一个元素的下标 StackPop(&st);//弹出左边第一个元素的下标 int right = StackTop(&st);//获取右边第一个元素 StackPop(&st);//弹出右边第一个元素的下标 int keyIndex = PartSort1(a, left, right);//快排并获取返回的key的位置 //这一次快排以后,数组的区间如下: //[left,keyIndex-1]keyIndex[keyIndex+1,right] //接下来只需要将左右区间的left,right压栈即可 if (keyIndex + 1 < right) //如果keyIndex + 1 >= right说明右区间没有元素,不用入栈 { StackPush(&st, right);//右区间的最后一个元素入栈 StackPush(&st, keyIndex + 1);//右区间第一个元素入栈 } if (left < keyIndex - 1) //如果left > keyIndex - 1说明左区间没有元素,不用入栈 { StackPush(&st, keyIndex - 1);//左区间最后一个元素入栈 StackPush(&st, left);//左区间第一个元素入栈 } } StackDestory(&st);//销毁栈 }
注意入栈和出栈的顺序不要弄错。如果是先取left后取right,则应该先入right,后入left。
理想情况下,对于一个大小为N的数组,每次递归的左右区间中的元素个数如果都相同,那么只需要调用log2N次即可。
但是当数组有序或者接近有序时,我们若是依然每次都选取最左边作为key,这就会导致左边区间根本就没有数,右边区间的数是除了key剩下的数,这样就必须调用N次递归了,那么快速排序的效率就会非常低。
三数取中的意思是取数组左边,中间,右边三个数中不是最大也不是最小的那个数,然后返回下标,接着交换这个数和最左边的数即可:
//三数取中 int GetMidIndex(int* a, int left, int right) { int mid = (left + right) / 2;//数组中间元素的下标 if (a[left] < a[mid]) { if (a[mid] < a[right]) { return mid; } else if (a[left] > a[right]) { return left; } else { return right; } } else { if (a[mid] > a[right]) { return mid; } else if (a[left] < a[right]) { return left; } else { return right; } } }
取中以后只需要在快排函数的最前面交换这个数和最左边的数:
int index = GetMidIndex(a, left, right);
Swap(&a[left], &a[index]);
通过三数取中后,递归后的key就不会在靠近中间的位置,减少了调用次数,提高了效率。
通过这张图不难看出来,当划分的左右子区间的元素个数很小的时候(一般为十几个),需要调用的次数会非常多,这时候使用别的排序算法比如直接插入排序比快速排序更加的高效。
//小区间优化的快速排序 void QuickSort(int* a, int left,int right) { if (left >= right) { return; } int keyIndex = PartSort1(a, left, right); //排序完成以后,从keyIndex分成三部分 //[left,keyIndex-1] keyIndex [keyIndex+1,right] if (keyIndex - 1 - left > 10)//如果区间范围小于10,则不递归 { QuickSort(a, left, keyIndex - 1); } else { InsertSort(a + left, keyIndex - 1 - left + 1);//使用直接插入排序 //a + left是左区间的起始下标,keyIndex - 1 - left + 1是左区间元素个数 } if (right - (keyIndex + 1)>10)//如果区间范围小于10,则不递归 { QuickSort(a, keyIndex + 1, right); } else { InsertSort(a + keyIndex +1, right-(keyIndex +1)+1);//使用直接插入排序 //a + keyIndex +1是右区间的起始下标,right-(keyIndex +1)+1是右区间元素个数 } }
如果没有优化:每次调用都会遍历一次传入的数组,所以调用一次的时间复杂度是O(N),而最好的情况就是key每次都是中间的位置,这样只需要调用log2N次,而最坏的情况需要调用N次,所以最好的时间复杂度是 O(Nlog2N),最坏的时间复杂度是O(N2)。
优化后,key一般都会在靠近中间的位置,所以优化后的时间复杂度一般为O(Nlog2N)
每次递归调用都会开辟空间,所以空间复杂度最好的情况是O(Nlog2N),最坏的是O(N2)。
平均来看:
时间复杂度为O(NlogN)
空间复杂度为O(NlogN)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。