⑤ 快速排序
实现快速排序需要在每一趟排序中找一个枢纽量pivotKey,并设置变量pivotIndex用来记录pivotKey应该放在哪个位置。
假定枢纽量为排序区间的最左元素(pivotKey = arr[ left ]),
在每一趟排序中,设置 两个标兵left与right,用right 从右往左 找 小于pivotKey的元素,找到后停下来,将该元素复制到left所在的位置;此时用left 从左到右找 大于pivotKey的元素 ,找到后停下来,将该元素复制到right所在的位置,以此循环。
当left与right相遇时,就将pivotKey插入到两个指针相遇的位置,该趟排序结束。
下一趟排序则以pivotKey所在的位置 为分界线,对两个区间进行递归快速排序操作。
实现一趟快速排序的代码为:
//快速排序每一趟排序的算法
public static int divideArr(int[] arr , int left , int right){
int pivotKey ;
pivotKey = arr[left]; //假定每趟排序的枢纽量为该区间的最左端元素,先用pivotKey将它保存起来
while( left < right ){
while( left < right && arr[right] >= pivotKey)
right -- ;
while( left < right && arr[left] <= pivotKey)
left ++ ;
}
arr[left] = pivotKey;//将pivotKey放置到arr[left]处
return left;
}
实现快速排序的全局操作代码为:
public static void quickSort(int[] arr, int left , int right){
int pivotIndex ; //枢纽量放在数组的哪个位置,以切割数组
if(left < right){
pivotIndex = divideArr(arr , left , right);
//通过以上的divideArr方法确定出枢纽量最后归位的位置
quickSort(arr , left , pivotIndex - 1 ); //对枢纽量左边的数进行快速排序
quickSort(arr , pivotIndex + 1 , right); //对枢纽量右边的数进行快速排序
}
}
⑥ 归并排序:
归并排序的一个特点,就是要借助辅助数组,这表明了归并排序的复杂度往往比较大。
实现归并排序的关键在于归并操作,以下是归并操作的代码,解析详细:
/**
* 该方法实现将arr[left....mid]与arr[mid+1....right]两个数列排序成一个数列
*/
public static void merge(int[] arr , int left , int mid , int right){
int[] temp = new int[arr.length];//创建一个辅助数组,用来装归并后的数列
int p1 = left ,p2 = mid + 1; //p1和p2作为数组两个区间的比较指针
int k = left; //k指针将元素按序存放到temp数组中
//(1)将arr[left....mid]与arr[mid+1....right]两个数列的元素进行逐一比较
while(p1 <= left && p2 <= right){
if(arr[p1] <= arr[p2])
temp[k++] = arr[p1];
else
temp[k++] = arr[p2];
}
//(2)如果一个序列的元素没有检测完,则直接把这个数列后面所有的元素加到合并的序列中
while(p1 <= left) temp[k++] = temp[p1++];
while(p2 <= right) temp[k++] = temp[p2++];
//(3)将辅助数组里的数据复制回原数组
for(int i = left ; i <= right ; i ++ )
arr[i] = temp[i];
}
归并排序的全局代码如下:
//实现归并排序(递归调用)
public static void mergeSort(int[] arr, int left ,int right){
if(left < right){
int mid = (left + right) / 2;
mergeSort(arr,left, mid);//对左侧进行递归排序
mergeSort(arr,mid+1 , right);//对右侧进行递归排序
merge(arr,left,mid,right);//对左右两侧进行归并操作
}
}
⑦ 基数排序:
几点说明:
1、基数排序是对传统桶排序的扩展,速度很快。在大规模数据的情形下,甚至比快速排序更快;
2、基数排序是经典的用空间换时间的排序方式,因此占用内存会比较大,对海量数据进行排序时,容易出现OutOfMemoryError的异常。
3、基数排序是稳定的排序。
4、对于有负数的数组,不能用基数排序进行排序(桶数组会越界)。如果非要进行负数排序,就要考虑绝对值和运算值反转等操作。
思路:
(1)定义一个二维数组int bucket[10 ][ ],表示10个“桶”(0-9),每个桶就是一个一维数组。为了防止数据在放入桶时溢出,每个一维数组(桶)的大小 定义为 排序数组arr[ ]的长度,即arr.length
(2)同时,为了记录10个桶中的每个桶中存放了多少个数据,需要定义一个一维数组int bucketElementCount[ ]来记录 该次排序中每个桶放入了多少个数据。比如 bucketElementCount[ 0 ] 表示 桶bucket[ 0 ]中放入数据的个数。
(3)此时按照个==>十==>百==>千==>万.....的顺序进行多轮排序。排序的轮次数等于最大数的位数。
① 遍历arr数组,取出arr中的每个元素,根据当前位(从个位开始)的值,将其放入二维数组bucket对应的桶里;
② 遍历二维数组bucket[ ][ ]中 存在元素的“桶”,严格按顺序 取出桶中的元素,重新放回arr中
(4)最后,为了获取数组中最大数的位数,可以编写maxRadix(int [ ] arr)方法,返回arr数组中最大元素的位数。
public static void radixSort(int[] arr) {
int maxLength = maxRadix(arr);
// 定义一个二维数组,表示10个桶(0-9),每个桶就是一个一维数组
/*
* 说明: 1、二维数组包含10个一维数组 2、为了防止数据在放入桶时溢出,每个一维数组(桶)的大小设置为arr.length
* 3、很明显,基数排序是用空间换时间的经典算法
*/
int[][] bucket = new int[10][arr.length];
/*
* 为了记录每个桶中实际存放了多少个数据, 需要定义一个一维数组来记录每个桶每一次排序放入的数据个数
* 比如:bucketElementCounts[0]表示bucket[0]桶中放入数据个数
*/
int[] bucketElementCounts = new int[10];
//按照个十百千万的顺序进行多轮排序
for (int w = 0, n = 1; w < maxLength; w++, n *= 10) {
// ① 遍历arr数组,取出arr中每个元素,根据个位的值,将其放入相应的桶中
for (int j = 0; j < arr.length; j++) {
int digitElement = arr[j] / n % 10; // 取出该元素的个位
bucket[digitElement][bucketElementCounts[digitElement]] = arr[j];
bucketElementCounts[digitElement]++;
// 难点:
}
// ② 遍历二维bucket中有元素的桶,按顺序取出桶中的元素,重新放回arr
int index = 0;
for (int k = 0; k < bucketElementCounts.length; k++) {
if (bucketElementCounts[k] != 0) {
for (int i = 0; i < bucketElementCounts[k]; i++) {
arr[index] = bucket[k][i];
index++;
}
bucketElementCounts[k] = 0;
for (int i = 0; i < bucket[k].length; i++)
bucket[k][i] = 0;
}
// 进行完一轮排序后,要将其置零,否则下一轮排序会出问题
}
System.out.println("第" + (w+1) + "轮排序的结果为: " +
Arrays.toString(arr));
}
}
// 编写一个方法,获取一个数组中最大元素的位数
public static int maxRadix(int[] arr) {
int max = arr[0];
for (int i = 0; i < arr.length; i++)
if (max < arr[i])
max = arr[i];
int maxLength = (max + "").length();
return maxLength;
}
⑧ 堆排序:
基本思想:
(1)将待排序序列构造成一个大顶堆
(2)此时,整个序列的最大值就是堆顶的根节点
(3)将其与末尾元素进行交换,此时末尾就是最大值
(4)然后将剩余n-1个元素重新构造成一个堆,就会得到n个元素的次小值,如此反复,就可以得到一个有序序列了。
注意:
(1)堆排序的前提是构造大(小)顶堆,所以首先要编写一个构造堆的方法,此处构造大顶堆。
(2)由于在堆排序中,排序数据构成一棵完全二叉树。对于完全二叉树,结点序号从0开始,则序号为 i 的结点,其左孩子(假设存在)的标号为 2 * i + 1 , 右孩子(假设存在)的标号为 2 * i + 2 。
(3)构造大顶堆的方法:
// 堆排序的前提是构造大(小)顶堆,所以在此编写一个相应的构造堆的方法
/**
* 功能: 完成将 i指向的非叶子结点为根的树 调整成堆(大顶堆)
*
* @param arr
* 待调整的数组
* @param i
* 表示非叶子结点在数组中的索引(在堆排序中,从 第1个非叶子结点 开始调整)
* @param length
* 表示对多少个元素继续进行调整,length自然是不断减少的
*/
public static void adjustHeap(int arr[], int i, int length) {
int temp = arr[i];// 首先取出当前元素的值,保存在临时变量中
// 此时开始调整
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
// k = i * 2 + 1 表示k从i的左子结点出发,不断地从“左指针域”去寻找 最大值,所以也要k = k * 2 + 1
if (k + 1 < length && arr[k] < arr[k + 1]) {
k++; // 走到了当前i的左结点后,要先拿该左结点与i的右结点作比较,寻找一个更大的值
// 所以如果右结点要大于左结点,那就把指针k移动到i的右结点,然后继续从左指针域去找最大值。
}
if (arr[k] > temp) { // 如果i节点的左(右)孩子值大于i的值,就把更大的值赋给i
arr[i] = arr[k];
i = k; // !!!i指向k是为了持续以上的for循环,继续向下比较
} else {
break;
// 为什么这里敢break?
// 因为我们对一棵树的调整是从下到上,从左往右的,所以调整当前结点时,它的后辈结点一定已经调整好了
// 即该结点的孩子结点对应的子树已经符合大顶堆的性质了
}
}
arr[i] = temp;
// for循环结束后,以i为根结点的树的最大值,已经放在了i的位置(顶部)。
//这一句相当于将原来 根结点i的值 替换到
}
在构造大顶堆方法的基础上,从最后一个非叶子结点开始,倒序 进行建堆。
建堆完成后,将最大元素与数组末尾元素交换位置。
由于交换了位置,因此破坏了以前堆的性质,因此要再进行一次堆的调整。
但此时调整堆只需要从根节点开始(原因见以下代码的注释,很详细)
// 编写一个堆排序的方法
public static void heapSort(int arr[]) {
System.out.println("堆排序开始:");
// 1、首先将无序序列构建成一个堆
for (int i = arr.length / 2 - 1; i >= 0; i--) {
adjustHeap(arr, i, arr.length); // 这里是初始化建堆操作
}
int temp;// 仅仅用于交换操作
// 2、进行了建堆操作后,将堆顶元素和末尾元素进行交换,将最大元素放到数组末端;
// 3、重新调整结构,在交换后再次建堆,然后继续进行上述交换,反复执行,使整个序列有序
for (int j = arr.length - 1; j > 0; j--) {
temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
adjustHeap(arr, 0, j);
// 这里是由于将堆顶元素和堆底元素进行交换后,堆的性质被破坏。
// 因此需要将堆顶元素向下进行调整,使其继续保持堆的性质。
// 之所以这里从0开始调整即可,是因为堆顶和堆底元素交换后,堆底元素被输出。
// 此时除了堆顶元素即0处元素,其他元素构成的树依然满足堆的性质。
}
System.out.println("排序完成后的数组为: " + Arrays.toString(arr));
}