快速排序可以说是冒泡排序升级过来的,或者说在冒泡排序中加入了分治的思想。和冒泡排序不同的是:冒泡排序在每一轮只把一个元素(最值)冒泡到数列的一端,而快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个小数列,就这样不断循环这个过程就变成了n多个小数列。
分治就是分而治之的意思,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
假如给定8个元素的数列,一般情况下冒泡排序需要比较8轮,每轮把一个元素移动到数列一端,时间复杂度是O(n^2)。
在分治法的思想下,原数列在每一轮被拆分成两部分,每一部分在下一轮又分别被拆分成两部分,直到不可再分为止。这样一共需要多少轮呢?平均情况下需要logn轮,因此快速排序算法的平均时间复杂度是 O(nlogn)。所以说分治是一种很强大的思想。
基准元素的选择:
一般情况下基准元素我们会选数列的第一个元素,但是有时候原数列是一个逆序数列那时间复杂度就成了O(n^2)级了,所以我们最好是随便选一个元素做基准元素,当然,即使是随机选择基准元素,每一次也有极小的几率选到数列的最大值或最小值所以快速排序的平均时间复杂度是 O(nlogn),最坏情况下的时间复杂度是 O(n^2)。
元素的移动:
选择好基准元素就要开始进行排序了,这里列举两种方法:
1.挖坑法:将基准元素挖出来,装桶里(值保存下来为pivot),记着这个坑位 index,先从右边开始找如果right里的值比pivot小,就挖出来填进index。然后这个坑index就到了right的位置。
此时,left左边绿色的区域代表着小于基准元素的区域。接下来,我们切换到left指针进行比较。如果left里的元素小于pivot,则left指针向右移动;如果元素大于pivot,则把left里的元素填入坑中。在当前数列中,7>4,所以把7填入index的位置。这时候元素7本来的位置成为了新的坑。同时,right向左移动一位。
此时,right右边橙色的区域代表着大于基准元素的区域。下面按照刚才的思路继续排序:8>4,元素位置不变,right左移。
直到right==left再把pivot填进index。1 2 3 5 6 8 7这样第一轮排序就结束了同时第一个基准元素也到了最佳位置上。接下来就是像刚才那样对前半段和后半段进行排序直到全部排序完:1 2 3 5 6 7 8。
下面是填坑法的代码:
void quickSort(int arr[],int startIndex,int endIndex)
{
// 递归结束条件:startIndex大等于endIndex的时候
if(startIndex >= endIndex)return;
// 进行一次排序 移动
int pivotIndex = partition(arr, startIndex, endIndex);
// 用分治法递归数列的两部分
quickSort(arr, startIndex, pivotIndex-1);//前半部分
quickSort(arr, pivotIndex +1,endIndex); //后半部分
}
int partition(int[] arr,int startIndex,int endIndex)
{
// 取第一个位置的元素作为基准元素
int pivot = arr[startIndex];
int left = startIndex;
int right = endIndex;
// 坑的位置,初始等于pivot的位置
int index = startIndex;
//大循环在左右指针重合或者交错时结束
while ( right >= left )
{
//right指针从右向左进行比较
while( right >= left )
{
if(arr[right] < pivot)
{
arr[left] = arr[right];
index = right;
left++;
break;
}
right--;
}
//left指针从左向右进行比较
while( right >= left )
{
if (arr[left]>pivot)
{
arr[right] = arr[left];
index = left;
right--;
break;
}
left++;
}
}
arr[index] = pivot;
return index;
}
2.交换法:挖坑法交换的是坑位和数而坑位是不用保存值的(相当于它是空着的),交换法交换的是数列的两个数。交换法开局和挖坑法相似,我们首先选定基准元素Pivot,并且设置两个指针left和right,指向数列的最左和最右两个元素。
接下来是从right指针开始,把指针所指向的元素和基准元素做比较。如果大于等于pivot,则指针向左移动;如果小于pivot,则right指针。停止移动,切换到left指针。在当前数列中,1<4,所以right直接停止移动,换到left指针,进行下一步行动。轮到left指针行动,把指针所指向的元素和基准元素做比较。如果小于等于pivot,则指针向右移动;如果大于pivot,则left指针停止移动。由于left一开始指向的是基准元素,判断肯定相等,所以left右移一位。
由于7 > 4,left指针在元素7的位置停下。这时候,我们让left和right指向的元素进行交换。
接下来重新切换到right向左移动。right先移动到8,8>4,继续左移。由于2<4,停止在2的位置。
直到left==right把基准元素和left或者说right里的元素交换,这样第一轮排序排完;3 1 2 4 5 6 8 7
void quickSort(int[] arr, int startIndex, int endIndex)
{
// 递归结束条件:startIndex大等于endIndex的时候
if (startIndex >= endIndex)return;
// 进行排序 移动并得到基准元素的正确位置
int pivotIndex = partition(arr, startIndex, endIndex);
// 根据基准元素,分成两部分递归排序
quickSort(arr, startIndex, pivotIndex -1);
quickSort(arr, pivotIndex + 1, endIndex);
}
int partition(int[] arr, int startIndex, int endIndex)
{
// 取第一个位置的元素作为基准元素
int pivot = arr[startIndex];
int left = startIndex;
int right = endIndex;
while( left != right)
{
//控制right指针比较并左移
while(left<right && arr[right] > pivot)right--;
//控制left指针比较并右移
while(left<right && arr[left] <= pivot)left++;
//交换left和right指向的元素
if(left<right)
{
int p = arr[left];
arr[left] = arr[right];
arr[right] = p;
}
}
//pivot和指针重合点交换
int p = arr[left];
arr[left] = arr[startIndex];
arr[startIndex] = p;
return left;
}
和挖坑法相比,指针交换法在partition方法中进行的元素交换次数更少。
以上代码都是用递归来实现的但是我们最好还是不要用递归,大部分递归都是可以用栈来代替,下面是代码再膜拜一下。。。or2
public class QuickSortWithStack
{
public static void quickSort(int[] arr, int startIndex, int endIndex)
{
// 用一个集合栈来代替递归的函数栈
Stack<Map<String, Integer>> quickSortStack = new Stack<Map<String, Integer>>();
// 整个数列的起止下标,以哈希的形式入栈
Map rootParam = new HashMap();
rootParam.put("startIndex", startIndex);
rootParam.put("endIndex", endIndex);
quickSortStack.push(rootParam);
// 循环结束条件:栈为空时结束
while(!quickSortStack.isEmpty())
{
// 栈顶元素出栈,得到起止下标
Map<String, Integer> param = quickSortStack.pop();
// 得到基准元素位置
int pivotIndex = partition(arr, param.get("startIndex"), param.get("endIndex"));
// 根据基准元素分成两部分, 把每一部分的起止下标入栈
if(param.get("startIndex") < pivotIndex -1)
{
Map<String, Integer> leftParam = new HashMap<String, Integer>();
leftParam.put("startIndex", param.get("startIndex"));
leftParam.put("endIndex", pivotIndex -1);
quickSortStack.push(leftParam);
}
if(pivotIndex + 1< param.get("endIndex"))
{
Map<String, Integer> rightParam = new HashMap<String, Integer>();
rightParam.put("startIndex", pivotIndex + 1);
rightParam.put("endIndex", param.get("endIndex"));
quickSortStack.push(rightParam);
}
}
}
private static int partition(int[] arr, int startIndex, int endIndex)
{
// 取第一个位置的元素作为基准元素
int pivot = arr[startIndex];
int left = startIndex;
int right = endIndex;
while( left != right)
{
//控制right指针比较并左移
while(left<right && arr[right] > pivot)
{
right--;
}
//控制right指针比较并右移
while( left<right && arr[left] <= pivot)
{
left++;
}
//交换left和right指向的元素
if(left<right)
{
int p = arr[left];
arr[left] = arr[right];
arr[right] = p;
}
}
//pivot和指针重合点交换
int p = arr[left];
arr[left] = arr[startIndex];
arr[startIndex] = p;
return left;
}
}
这里推荐个公众号叫程序员小灰,以上代码和图片都转载自程序员小灰,是真好。