快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法。其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
这里我以升序为例。
简单来说:一趟排序的本质实际上是将一个或几个数据放到它应该在的位置上。
例如:冒泡排序:每一趟都能将最大的数放到最后的位置。
插入排序:每次插入的数与前面比较,满足条件就插入,不满足就移动。
选择排序:每次选出最大/最小放到最前面或最后面。
堆排序:堆顶可以得到最值,与最后的元素交换后,使得这个最值排在正确位置。向下调整是为了下一次的排序。
而对于快速排序:是先用keyi(下标)标记数组中的一个位置,对于key=arr[keyi],比它小的放到都它的左边,比它大的都放到它的右边,完成这样的一趟排序,key的位置就是它最终的位置。
然后再通过递归,对key左右分别进行快排。

为了方便讲解,我们先将keyi=left,使得每次key都是最左边的元素。
并用left和right分别标记数组的最左/右端。(左边为key,右边先找,后续讲解原因)。
对于1-10的十个数,我们第一趟快排的目的是将6排到下标为5的位置上,并且保证它左边的数都小于6,右边的数都大于6.(这是为了保证,分别对其左右两端排序后,连接上这个6,整体的数组为升序,从而达到排序目的)。
right从最右端开始找比key==6小的数,找不到就--right,直到用right标记小的这个数。
right找到小后,left开始从左边找比key==6大的数,找不到就++left。只要满足left<right就可以一直找,直到left和right相遇。
然后将相遇点的数与key交换,此时key就来到了正确的位置。因为刚刚left和right查找并交换的过程,使得比key大的数都到了key左边,而比key小的数也交换到了右边。
void QuickSort(int* arr, int left,int right)
{
int keyi = left;
while (left < right)
{
//先从右边开始找
while ((left<right) && arr[right] >= arr[keyi])//右边找小
{
--right;
}
while ((left<right) && arr[left] <= arr[keyi])//左边找大
{
++left;
}
Swap(&arr[left], &arr[right]);
}
//出来后left == right ,交换它与key
Swap(&arr[left], &arr[keyi]);
keyi = left;//此时keyi在正确的位置
}
当然,注意一点,最后交换了重合位置与key的值之后,还要让keyi=left,更新一下keyi的位置,便于后续递归。

观察打印后的结果,6排到了正确的位置,比它小的在它左边,比它大的在它右边。
我们实现快排有3个参数,一个是指向数组第一个元素的指针,另外是下标left和right。
为了对key左右分别递归快排,arr数组不变,需要记录左右两部分各自的left和right位置。
但是因为left和right在排序过程中会改变,因此可以定义begin和end来保存起始和结束的位置。
然后对 [begin,keyi-1] 和 [keyi+1,end]分别递归进行快排。

当排序区间[left,right]只有一个元素或没有元素时返回。即left>=right时。
void QuickSort(int* arr, int left,int right)
{
if (left >= right)
{
return;
}
int begin = left;
int end = right;
int keyi = left;
while (left < right)
{
//先从右边开始找
while ((left<right) && arr[right] >= arr[keyi])//右边找小
{
--right;
}
while ((left<right) && arr[left] <= arr[keyi])//左边找大
{
++left;
}
Swap(&arr[left], &arr[right]);
}
//出来后left == right ,交换它与key
Swap(&arr[left], &arr[keyi]);
keyi = left;//此时keyi在正确的位置
QuickSort(arr, begin, keyi - 1);
QuickSort(arr, keyi + 1, end);
}

有一个要注意的点,加入数组中不止一个值为6,当right找小时不能停下,否则当left也为6时,就会产生2个6一直交换,66...66...66.... 导致死循环,因此必须严格找小/大。
还有一件事,right找小时不能闷着头找,一直--,同时还要保证left<right。
假设每次选到的left位置的key都是这个数组中大小处于中间位置的数据,那么可以将它左右两边的n-1个数据平均分成两组,然后递归快排。
这两边的元素若仍满足,可以再对n-3个分成4组。(此时又排好了2个)

对于100W个数据,可以分出来约20层,每层有2^n组,每组可以额外少排2^(n-1)次方个数,当然还要加上之前的,大约是2^n个。
换句话说,对于快排整体来说,总共要递归20层,每层大概有n个(实际上比n小一点)元素,因此其时间复杂度可认为是O(NlogN)

当数组已经是有序(不论升序还是降序),此时keyi标记的数据是数组中的max或min,在进行下一层递归时,一侧为0个元素,另外一侧为n-1个元素,也就是说,要递归N层!

首先,N足够大时,递归N层会导致栈溢出,程序直接崩掉。
然后,这个快速排序就退化成了冒泡排序 ,第一层遍历N个,第二层N-1个,……直到最后1个。
为等差数列,计算后时间复杂度为O(N)。
一个是代码实现上需要注意的几点,是否出现死循环/越界,还有一个优化点就是有序数组退化的问题,时间复杂度大,且可能导致栈溢出。
这些问题的本质是因为我们默认keyi的位置是left,当知到默认为left时,我们当然可以通过有序数组的特殊例子来“推翻”快速排序。下面给出2中优化方法。
随机数优化方法就是keyi的位置不固定为left,而是[left,right]中的任一位置,这样就可以嘴大尺度避免有序的特殊情况,(当然顺序的情况也会出现,但概率极低,只有数组有序,且随机到left或right时才会出现)。

void QuickSort(int* arr, int left,int right)
{
if (left >= right)
{
return;
}
int begin = left;
int end = right;
//随机数法,得到一个下标
int rand = Qsortrand(arr, left, right);
Swap(&arr[left], &arr[rand]);
int keyi = left;
while (left < right)
{
//先从右边开始找
while ((left<right) && arr[right] >= arr[keyi])//右边找小
{
--right;
}
while ((left<right) && arr[left] <= arr[keyi])//左边找大
{
++left;
}
Swap(&arr[left], &arr[right]);
}
//出来后left == right ,交换它与key
Swap(&arr[left], &arr[keyi]);
keyi = left;//此时keyi在正确的位置
QuickSort(arr, begin, keyi - 1);
QuickSort(arr, keyi + 1, end);
}
用区间的开始left,加上范围在区间的差(right-left)内的随机数,使得随机数落在区间范围内。
得到随机数的下标后,它的值可能是数组中的任意大小,因此便于后面的递归排序。
交换 left 与 rand两个值,keyi仍然指向left,但此时arr[left]已经是随机的了
因为随机数法还是有可能选到最大/最小的数据的,因此又有人提出来三数取中法。
即在arr[left]/arr[right]/arr[mid]中选出中间大小的数,既保证了随机选择,又避免了取到最大/最小,进一步提升了效率。
//三数取中法优化快排
int GetMidNumi(int* arr, int left, int right)
{
int mid = (left + right) / 2;
// 先取出 最左端、最右端、中间的数据
// 比较出大小后返回的是下标
if (arr[left] > arr[right])
{
if (arr[mid] < arr[right])
{
return right;
}
else if (arr[left] > arr[mid])
{
return mid;
}
else
{
return left;
}
}
else
{
if (arr[left] > arr[mid])
{
return left;
}
else if (arr[right] > arr[mid])
{
return mid;
}
else
{
return right;
}
}
}
返回的是3个数中 中间大小的下标。

接收后,同样将left与这个中间数的值交换,这样left指向的值就是随机的,且不为最大/最小。
经过上面的优化,避免了最坏情况,快排的时间复杂度可认为是O(NlogN)。
1、普通情况:此时,right找小,找到后停下,left找大,找到后停下,互相交换,此时left和right没有相遇,一直重复上述过程。
2、左遇右:此时右边已经停下,则arr[right]<key是成立的,左边一直找大,没有找到,直到与right相遇,此时二者都指向一个比key小的数。
3、右遇左:上一次普通情况交换数据后,arr[left]<key,此时右边找小,没找到,直到与left重合,此时二者都指向一个比key小的值。(这里有一种特殊情况,如果是升序,右边也找不到小,直到与左边重合,此时都指向第一个元素)。
找到key该在的位置后,交换arr[keyi]与这个位置的值即可排好key。
如果是左key左先走,在一个比key大的位置停留,若此时右边再走,与left重合,二者指向的值比key大,再交换就发生错误了。

整体思想上与Hoare方法类似,效率也差不多,但可能比较好理解一点。
先将arr[left]拿出来保存在tmp中,此时left的位置可以看作是一个坑,用hole标记,之后的操作就是不断的填坑,挖坑,再填坑。
因为坑在左边,所以从右边开始找小,找到后arr[hole]=arr[right],将右边这个小于key的值填入坑中,然后hole=right,更新坑的位置,然后坑就在右边了。
之后不断重复,因为left和right一定有一个与hole重合,所以left==right时,hole也在相同的位置。因此arr[hole]=tmp最终把tmp的保存的值填入坑中即可。然后递归hole两侧进行后续快排。
// 挖坑法快排
void QsortHole(int* arr, int left, int right)
{
if (left >= right)
{
return;
}
int begin = left;
int end = right;
int mid = GetMidNumi(arr, left, right);
Swap(&arr[mid], &arr[left]);
int tmp = arr[left];
int hole = left;
while (left < right)
{
// 右边找小
while (left < right && arr[right] >= tmp)
{
right--;
}
// 右边填左边的坑
arr[hole] = arr[right];
hole = right;
// 左边找大
while (left < right && arr[left] < tmp)
{
++left;
}
// 左边填右边的坑
arr[hole] = arr[left];
hole = left;
}
arr[hole] = tmp;
QsortHole(arr, begin, hole - 1);
QsortHole(arr, hole + 1, end);
}
要注意的是一开始要用tmp保存值,填坑过程为赋值,并且hole也要更新。

利用prev、cur两个数组下标,也可看作是指针,二者本质相同。
起始状态,prev和key都指向最左边的元素,cur指向下标为1的。
然后先用cur来遍历整个数组找小,如果arr[cur]>=key,则该位置较大,不用交换++cur;如果arr[cur]<key,该位置较小,需要交换。
先++prev,意义是要调整的前面的,较小的数据数+1,因此prev要先+1.然后Swap(&arr[prev],&arr[cur])。
重复上述过程。

通过观察我们可以发现,prev与cur之间如果存在数据,那么这些数据都是大于key时cur多走的。
当cur>key时,cur就遍历完了整个数组,将所有小于key的值都交给了arr[prev],此时prev及其左侧的数均<key(起始位置除外),最后再交换一下起始位置和当前prev的位置。
最后再以prev为分隔,分别递归两侧,进行后续快排。
// 快慢指针法快排
void QSortByPtrs(int* arr, int left, int right)
{
if (left >= right)
{
return;
}
int begin = left;
int end = right;
int midi = GetMidNumi(arr, left, right);
printf("%d\n", arr[midi]);
Swap(&arr[left], &arr[midi]);
int key = arr[left];
int cur = left + 1;
int prev = left;
while (cur <= right)
{
while (cur <= right && arr[cur] >= key)
{
++cur;
}
if (cur <= right && arr[cur] < key)
{
++prev;
if (prev != cur)
Swap(&arr[prev], &arr[cur]);
++cur;
}
}
//cur遍历完后,prev指向的位置就是key的最终位置
Swap(&arr[prev], &arr[begin]);
//递归实现后续快排
QSortByPtrs(arr, begin, prev - 1);
QSortByPtrs(arr, prev+1, end);
}
要注意的是cur和left不能只是1和0,而是要根据left去取值,否则对右边的元素递归会产生错误。
++cur前保证cur<=right,防止越界访问。
最后将begin位置与prev交换,完成一趟快排,然后递归实现后续快排。
本文讲了Hoare、挖坑法、快慢指针法三种递归方法完成快速排序,它们的递归过程是相同的,可以将递归结束条件和后续递归过程封装起来,然后在中间调用3种方法的函数,使逻辑更清晰。

封装中只写结束条件和递归过程。并用一个keyi接收之前3种方法排序后key的下标位置。
1、Hoare方法返回keyi的位置。
2、挖坑法返回最终坑的位置。
3、快慢指针法返回prev的位置。
返回排好的那个数的位置后,再用left,keyi,right进行递归,从而完成快排的递归封装。
目录
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
总的来说,我对ruby还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
我正在尝试设置一个puppet节点,但rubygems似乎不正常。如果我通过它自己的二进制文件(/usr/lib/ruby/gems/1.8/gems/facter-1.5.8/bin/facter)在cli上运行facter,它工作正常,但如果我通过由rubygems(/usr/bin/facter)安装的二进制文件,它抛出:/usr/lib/ruby/1.8/facter/uptime.rb:11:undefinedmethod`get_uptime'forFacter::Util::Uptime:Module(NoMethodError)from/usr/lib/ruby
我想了解Ruby方法methods()是如何工作的。我尝试使用“ruby方法”在Google上搜索,但这不是我需要的。我也看过ruby-doc.org,但我没有找到这种方法。你能详细解释一下它是如何工作的或者给我一个链接吗?更新我用methods()方法做了实验,得到了这样的结果:'labrat'代码classFirstdeffirst_instance_mymethodenddefself.first_class_mymethodendendclassSecond使用类#returnsavailablemethodslistforclassandancestorsputsSeco
我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer
设置:狂欢ruby1.9.2高线(1.6.13)描述:我已经相当习惯在其他一些项目中使用highline,但已经有几个月没有使用它了。现在,在Ruby1.9.2上全新安装时,它似乎不允许在同一行回答提示。所以以前我会看到类似的东西:require"highline/import"ask"Whatisyourfavoritecolor?"并得到:Whatisyourfavoritecolor?|现在我看到类似的东西:Whatisyourfavoritecolor?|竖线(|)符号是我的终端光标。知道为什么会发生这种变化吗? 最佳答案
我已经从我的命令行中获得了一切,所以我可以运行rubymyfile并且它可以正常工作。但是当我尝试从sublime中运行它时,我得到了undefinedmethod`require_relative'formain:Object有人知道我的sublime设置中缺少什么吗?我正在使用OSX并安装了rvm。 最佳答案 或者,您可以只使用“require”,它应该可以正常工作。我认为“require_relative”仅适用于ruby1.9+ 关于ruby-主要:Objectwhenrun
我有一个具有一些属性的模型:attr1、attr2和attr3。我需要在不执行回调和验证的情况下更新此属性。我找到了update_column方法,但我想同时更新三个属性。我需要这样的东西:update_columns({attr1:val1,attr2:val2,attr3:val3})代替update_column(attr1,val1)update_column(attr2,val2)update_column(attr3,val3) 最佳答案 您可以使用update_columns(attr1:val1,attr2:val2
我不确定传递给方法的对象的类型是否正确。我可能会将一个字符串传递给一个只能处理整数的函数。某种运行时保证怎么样?我看不到比以下更好的选择:defsomeFixNumMangler(input)raise"wrongtype:integerrequired"unlessinput.class==FixNumother_stuffend有更好的选择吗? 最佳答案 使用Kernel#Integer在使用之前转换输入的方法。当无法以任何合理的方式将输入转换为整数时,它将引发ArgumentError。defmy_method(number)