续论秋招中的排序(排序法汇总-------上篇)(由于篇幅过大),下面我们继续学习。
待续
(原创,转发须注明原处)
5、快速排序
快速排序在面试中经常被问到(包括各种改进思路),此排序算法可以说是用得最为广泛的排序算法,是对冒泡排序的一种改进,每次交换是跳跃式的。其比较易于实现,同时它可以处理多种不同的输入数据,许多情况下所消耗的资源也较其他排序算法少。理想情况下,其只使用一个小的辅助栈,对N个数据项排序的平均时间复杂度为O(NlogN),并且它内部的循环很小,缺点就是不稳定的排序算法,在最坏的情况下时间复杂度为O(N^2)。其实过程很简单,想想就明白了,所以不想再细说,图解比语言更明确更精准,下面给出一轮划分的图解,接着递归两边的部分即可,图如下:
即划分后,key=32的最终位置的左边的所有数据值都小于(或等于)key,右边的所有数据值都大于(或等于)key。
代码如下:
int partion(int a[], int left, int right)//划分操作 { int lleft = left, rright = right, key = a[lleft]; while (lleft < rright) { while (lleft < rright && a[rright] >= key)//找右边第一个小于key的 rright--; a[lleft] = a[rright]; while (lleft < rright && a[lleft] <= key)//找左边第一个大于key的 lleft++; a[rright] = a[lleft]; } a[lleft] = key; return lleft;//key的最终位置 }
递归:
void Qsort(int a[], int left, int right) { if (left >= right)//递归结束条件 return; int index = partion(a, left, right); Qsort(a, left, index - 1);//递归key左边区域 Qsort(a, index + 1, right);//递归key右边区域 }
非递归:
void qsort(int a[], int left, int right) { std::stack<int> st; if (left < right) { int mid = partion(a, left, right); if (left < mid - 1) //将左边边界入栈 { st.push(left); st.push(mid - 1); } if (mid + 1 < right)//将右边边界入栈 { st.push(mid + 1); st.push(right); } while (!st.empty()) //如果栈不为空,即排序未结束 { int q = st.top(); st.pop(); int p = st.top(); st.pop(); mid = partion(a, p, q);//继续划分 if (p < mid - 1) { st.push(p); st.push(mid - 1); } if (mid + 1 < q) { st.push(mid + 1); st.push(q); } } } }
在最坏的情况下,如果要排序的序列是有序的,则快速排序将退化为冒泡排序,时间复杂度变为O(N^2);在最好的情况下,每次划分过程都恰好把文件分割成两个大小完全相等的部分,则时间复杂度为O(NlogN)。
改进算法:
上面我们也提到了,在最坏的情况下,如果待排序列已经有序了,则快排会变得非常低效。下面将会介绍改进的方法:
改进选取的参考枢纽元素:1、选取随机数作为枢轴。但是随机数的生成本身是一种代价,根本减少不了算法其余部分的平均运行时间。2、使用左端,右端和中心的中值做为枢轴元(即三者取中)。3、每次选取数据集中的中位数做枢轴。
改进代码:
void exchange(int*a, int *b) { int tmp; tmp = *a; *a = *b; *b = tmp; } void compexch(int* a, int* b) { if (*a > *b) exchange(a, b); } int partion(int a[], int left, int right)//划分操作 { int lleft = left + 1, rright = right - 1; int key; //三者取中法 int mid = (left + right) / 2; compexch(&a[left], &a[mid]); compexch(&a[left], &a[right]); compexch(&a[mid], &a[right]); key = a[mid]; while (lleft < rright) { while (lleft < rright && a[rright] >= key)//找右边第一个小于key的 rright--; a[lleft] = a[rright]; while (lleft < rright && a[lleft] <= key)//找左边第一个大于key的 lleft++; a[rright] = a[lleft]; } a[lleft] = key; return lleft;//key的最终位置 } void quicksort(int a[], int left, int right) //小区域排序采用插入排序方法(对于有序序列的排序效率较高O(N)),三者取中 { if (left >= right)//递归结束条件 return; if (right - left <= 5)//当区域段长度小于5时,改用插入排序法 { insertion(a, right-left+1); return; } int i; i = partion(a, left, right); quicksort(a, left, i - 1); quicksort(a, i + 1, right); }
下面我们继续考虑另外方面的改进,当待排序的序列中有大量的重复元素时,标准的快排又变得极其低效(哎呀,问题怎么这么多啊,烦不烦啊。。。。)。嘿嘿,当然,有解决的方法了,最直观的想法是将序列划分为三部分(三路划分)而不再是两部分了,即比划分元素小的部分、比划分元素大的部分和与划分元素相等的部分。
三路划分仅仅在标准的快排下稍作改动:遍历时将遇到的左边区域中的与划分元素相等的元素放到序列的最左边,将遇到的右边区域中的与划分元素相等的元素放到序列的最右边。继而,当两个扫描的指针相遇时,序列中与划分元素相等的元素的位置就精确定位了。对于重复的元素的额外工作量只与所找到的重复的元素的个数呈线性相关;即使在没有重复的元素的情况下,在方法没有额外的开销,效果也很好。
代码:
void quicksort(int a[], int left, int right) //小区域排序采用插入排序方法,三者取中,三路划分 { if (left >= right) return; int lleft = left - 1, rright = right , k, p=left-1, q=right; int key; if (right - left <= 1) { insertion(a, right-left+1); return; //递归返回条件 } int mid = (left + right) / 2; compexch(&a[left], &a[mid]); compexch(&a[left], &a[right]); compexch(&a[mid], &a[right]); key = a[right]; while(true) { while (a[++lleft]< key); while (key< a[--rright]) { if (rright == left) { break; } } if (lleft >= rright) { break; } exchange(&a[lleft], &a[rright]); if (a[lleft]== key) { p++; exchange(&a[p], &a[lleft]); } if (a[rright]== key) { q--; exchange(&a[q], &a[rright]); } } exchange(&a[lleft], &a[right]); lleft = lleft - 1; rright = lleft + 1; for (k = left; k <= p; k++, lleft--) { exchange(&a[k], &a[lleft]); } for (k = right - 1; k >= q; k--, rright++) { exchange(&a[k], &a[rright]); } quicksort(a, left, lleft); quicksort(a, rright, right); }