简述
高级排序算法:希尔排序和快速排序。
其执行效率分别为希尔排序的O(N*(logN)^2)和快速排序的O(N*logN)。
归并排序相较于简单排序中冒泡排序、选择排序和插入排序的执行速度要快,但是需要原始数据空间的两倍。而希尔排序和快速排序不需要大量的辅助空间。
具体实现
希尔排序
package com.jikefriend.arraysort;
/**
* 希尔排序,其基于插入排序,不适合大规模数据排序
*
* 希尔排序通过加入插入排序中元素之间的间隔,并在这些有间隔的元素中进行插入排序,
* 从而使数据项能大跨度地移动。当这些数据项排过一趟序后,希尔排序算法减小数据项
* 的间隔再进行排序,依此进行下去
*
* 进行这些排序时数据项之间的间隔称为增量
* 对于10个元素,通过 h = h * 3 + 1 确定间隔,有4和1
* 间隔为4时对(0,4,8)(1,5,9)(2,6)(3,7)分别排序,实际不是按此分组排序,
* 而是先排第一组的前两个数据项,然后是第二组的前两个数据项,以此类推,
* 当所有组的前两个数据项有序后,算法返回再对三个数据项的组进行排序,因此4-增量的实际执行顺序是
* (0,4)(1,5)(2,6)(3,7)(0,4,8)(1,5,9)
* 间隔为1时,排序后,算法结束
*
* 排序间隔序列的子数组,逐渐减小间隔,直至为1
* 注意:后续的排序不撤销前期排序的结果
*/
public class ShellSort {
private int[] theArray;
private int nElems;
public ShellSort(int max)
{
theArray = new int[max];
nElems = 0;
}
public void insert(int value)
{
theArray[nElems++] = value;
}
public void display()
{
System.out.print("A=");
for (int i = 0; i < nElems; i++)
System.out.print(theArray[i] + " ");
System.out.println();
}
public void shellSort()
{
int inner, outer;
int temp;
int h = 1;
while (h <= nElems / 3) //找到间隔序列的h初始值
h = h * 3 + 1;
while (h > 0) //根据公式 h = (h - 1) / 3 递减,直到h = 1
{
for (outer = h; outer < nElems; outer++) //以h为间隔对数组排序
{
temp = theArray[outer];
inner = outer;
while (inner > h - 1 && theArray[inner - h] >= temp)
{
theArray[inner] = theArray[inner - h];
inner -= h;
}
theArray[inner] = temp;
}
h = (h - 1) / 3;
}
}
public static void main(String[] args) {
int maxSize = 10;
ShellSort arr = new ShellSort(maxSize);
for (int i = 0; i < maxSize; i++) {
arr.insert((int)(Math.random() * 99));
}
arr.display();
arr.shellSort();
arr.display();
}
}
划分
package com.jikefriend.arraysort;
/**
* 划分
* 划分数据是把数据分成两组,
* 使所有关键字大于特定值的数据项在一组,
* 所有关键字小于特定值的数据项在一组。
*
* 注意:
* 划分是不稳定的,即每一组中的数据项并不是按照它原来的顺序排列的。
* 事实上,划分往往会颠倒组中一些数据项的顺序。
*/
public class PartitionArray {
private int[] theArray;
private int nElems;
public PartitionArray(int max)
{
theArray = new int[max];
nElems = 0;
}
public void insert(int value)
{
theArray[nElems++] = value;
}
public int size()
{
return nElems;
}
public void display()
{
for (int i = 0; i < nElems; i++)
System.out.print(theArray[i] + " ");
System.out.println();
}
/**
* 划分算法由两个指针开始工作,分别指向数组的两头。
* 左边的指针向右移动,右边的指针向左移动。
* @param left
* @param right
* @param pivot
* @return
*/
public int partition(int left, int right, int pivot)
{
int leftPtr = left - 1;
int rightPtr = right + 1;
while (true)
{
/** leftPtr当前的值如果比特定值小,则继续右移,因为这个数据项已经处在正确的一侧,直至找到比特定值大的数据项停止移动 */
while (leftPtr < right && theArray[++leftPtr] < pivot) //找出比特定值大的数据项
;
/** rightPtr当前的值如果比特定值大,则继续左移,因为这个数据项已经处在正确的一侧,直至找到比特定值小的数据项停止移动 */
while (rightPtr > left && theArray[--rightPtr] > pivot) //找出比特定值小的数据项
;
/** 当两个while都退出循环时,leftPtr和rightPtr都指向在数组的错误一方位置上的数据,交换这两个数据 */
if (leftPtr >= rightPtr) //如果两个指针相遇,则划分结束
break;
else //如果两个指针还没有相遇,则交换两个数据项
swap(leftPtr, rightPtr); //使小的数据项在特定值的左侧,大的数据项在右侧
}
return leftPtr;
}
private void swap(int dex1, int dex2)
{
int temp = theArray[dex1];
theArray[dex1] = theArray[dex2];
theArray[dex2] = temp;
}
public static void main(String[] args) {
int maxSize = 16;
PartitionArray arr = new PartitionArray(maxSize);
for (int i = 0; i < maxSize; i++) {
arr.insert((int)(Math.random() * 199));
}
arr.display();
int pivot = 99;
System.out.print("Pivot is " + pivot);
int size = arr.size();
int partDex = arr.partition(0, size - 1, pivot);
System.out.println(", Partition is at index " + partDex);
arr.display();
}
}
快速排序
使用最右端数据项作为枢纽值
package com.jikefriend.arraysort;
/**
* 快速排序,O(N*logN),最坏O(N^2)
* 基本步骤:
* 1、使用划分算法把数组或子数组划分成左边(较小的关键字)的一组和右边(较大的关键字)的一组;
* 2、调用自身对左边的一组进行排序;
* 3、再次调用自身对右边的一组进行排序。
*
* 基值情况:当数组只有一个数据项时,认为已经有序。
*
* 目标值(枢纽)的选取思想:
* 1、应该选择具体的一个数据项的关键字的值作为枢纽;
* 2、可以选择任意一个数据项作为枢纽。为了简便,我们假设总是选择划分的子数组最右端的数据项作为枢纽;
* 3、划分完成之后,如果枢纽被插入到左右子数组之间的分界处,那么枢纽就落在排序之后的最终位置上。
*/
public class QuickSort1 {
private int[] theArray;
private int nElems;
public QuickSort1(int max)
{
theArray = new int[max];
nElems = 0;
}
public void insert(int val)
{
theArray[nElems++] = val;
}
public void display()
{
for (int i = 0; i < nElems; i++)
System.out.print(theArray[i] + " ");
System.out.println();
}
public void quickSort()
{
recQuickSort(0, nElems - 1);
}
private void recQuickSort(int left, int right)
{
if (right - left <= 0) //当数组的大小 <= 1时,认为已经有序
return;
else
{
int pivot = theArray[right]; //选择数组的最右端数据项作为枢纽值
int partition = partition(left, right, pivot); //划分数组,使小于枢纽值的数据项在左边子数组,大于枢纽值的在右边子数组
recQuickSort(left, partition - 1); //递归调用该方法对左边数组排序
recQuickSort(partition + 1, right); //递归调用该方法对右边数组排序
}
}
/**
* 根据枢纽值划分数组
* @return 完成划分后枢纽值的位置
*/
private int partition(int left, int right, int pivot)
{
int leftPtr = left - 1;
int rightPtr = right;
while (true)
{
while (theArray[++leftPtr] < pivot) //由于枢纽值选取数组最右端,所以leftPtr不会发生数据越界
;
while (rightPtr > 0 && theArray[--rightPtr] > pivot)
;
if (leftPtr >= rightPtr)
break;
else
swap(leftPtr, rightPtr);
}
swap(leftPtr, right); //right是枢纽值所在的位置,即数组最右端,完成划分后,将枢纽值保存在分界处
return leftPtr;
}
private void swap(int dex1, int dex2)
{
int temp = theArray[dex1];
theArray[dex1] = theArray[dex2];
theArray[dex2] = temp;
}
public static void main(String[] args) {
int maxSize = 10;
QuickSort1 arr = new QuickSort1(maxSize);
for (int i = 0; i < maxSize; i++) {
arr.insert((int)(Math.random() * 99));
}
arr.display();
arr.quickSort();
arr.display();
}
}
使用三数据项取中法选取枢纽值
package com.jikefriend.arraysort;
/**
* 快速排序,O(N*logN),最坏O(N^2)
* “三数据项取中”划分
* 找到数组里第一个、最后一个以及中间位置数据项的居中数据项值,作为枢纽值。
*
* 由于在选择枢纽的同时,对该三个数据项进行了排序,保证了最左端的数据项小于枢纽值,
* 最右端的大于枢纽值,两端指针避免的越界。
*/
public class QuickSort2 {
private int[] theArray;
private int nElems;
public QuickSort2(int max)
{
theArray = new int[max];
nElems = 0;
}
public void insert(int val)
{
theArray[nElems++] = val;
}
public void display()
{
for (int i = 0; i < nElems; i++)
System.out.print(theArray[i] + " ");
System.out.println();
}
public void quickSort()
{
recQuickSort(0, nElems - 1);
}
private void recQuickSort(int left, int right)
{
int size = right - left + 1;
if (size < 10) //当数组的大小 < 10时,使用插入排序
insertionSort(left, right);
else
{
int pivot = medianOf3(left, right); //三数据项取中来选取枢纽值
int partition = partition(left, right, pivot); //划分数组,使小于枢纽值的数据项在左边子数组,大于枢纽值的在右边子数组
recQuickSort(left, partition - 1); //递归调用该方法对左边数组排序
recQuickSort(partition + 1, right); //递归调用该方法对右边数组排序
}
}
private int medianOf3(int left, int right)
{
int center = (left + right) / 2;
if (theArray[left] > theArray[center]) //对left和center排序
swap(left, center);
if (theArray[left] > theArray[right]) //对left和right排序
swap(left, right);
if (theArray[center] > theArray[right]) //对center和right排序
swap(center, right);
swap(center, right - 1); //将选定的枢纽值放到数组最右端-1
return theArray[right - 1];
}
/**
* 根据枢纽值划分数组
* @return 完成划分后枢纽值的位置
*/
private int partition(int left, int right, int pivot)
{
int leftPtr = left;
int rightPtr = right - 1;
while (true)
{
while (theArray[++leftPtr] < pivot) //由于枢纽值选取数组最右端,所以leftPtr不会发生数据越界
;
while (theArray[--rightPtr] > pivot)
;
if (leftPtr >= rightPtr)
break;
else
swap(leftPtr, rightPtr);
}
swap(leftPtr, right - 1); //right - 1是枢纽值所在的位置,即数组最右端-1,完成划分后,将枢纽值保存在分界处
return leftPtr;
}
private void swap(int dex1, int dex2)
{
int temp = theArray[dex1];
theArray[dex1] = theArray[dex2];
theArray[dex2] = temp;
}
/**
* 插入排序,处理小划分
*/
private void insertionSort(int left, int right)
{
int in, out;
for (out = left + 1; out <= right; out++)
{
int temp = theArray[out];
in = out;
while (in > left && theArray[in -1] >= temp)
{
theArray[in] = theArray[in - 1];
--in;
}
theArray[in] = temp;
}
}
public static void main(String[] args) {
int maxSize = 10;
QuickSort2 arr = new QuickSort2(maxSize);
for (int i = 0; i < maxSize; i++) {
arr.insert((int)(Math.random() * 99));
}
arr.display();
arr.quickSort();
arr.display();
}
}
摘自《 Java 数据结构 与 算法 (第二版)》 [美] Robert Lafore 著