目录
一、归并排序
归并排序算法将数组分为两半,然后对每个部分持续递归划分子数组。直到每组只有一个元素后开始合并排序,接下来就是不断合并排序合并排序....最终两部分排好序后直接合并即可:
代码实现如下:
public class MergeSort {
public static void mergeSort(int[] list){
if(list.length > 1) {
// list前半部分副本 --> firstHalf
int[] firstHalf = new int[list.length / 2];
System.arraycopy(list, 0, firstHalf, 0, list.length / 2);
mergeSort(firstHalf); // 递归
int secondHalfLength = list.length - list.length / 2;
// list后半部分副本 --> secondHalf
int[] secondHalf = new int[secondHalfLength];
System.arraycopy(list, list.length / 2, secondHalf, 0, secondHalf.length);
mergeSort(secondHalf); // 递归*2
merge(firstHalf, secondHalf, list); // 两副本排好序后归并成新数组list即可
}
}
// merge方法归并两个数组为temp
public static void merge(int[] list1, int[] list2, int[] temp){
int current1 = 0; // list1 元素下标
int current2 = 0; // list2 元素下标
int current3 = 0; // temp 元素下标
while(current1 < list1.length && current2 < list2.length){
if(list1[current1] < list2[current2]){
temp[current3++] = list1[current1++]; // 小元素在list1中
}else{
temp[current3++] = list2[current2++]; // 小元素在list2中
}
}
while(current1 < list1.length){
temp[current3++] = list1[current1++]; // list1中有未移元素 --> temp
}
while(current2 < list2.length){
temp[current3++] = list2[current2++]; // list2中有未移元素 --> temp
}
}
public static void main(String[] args){
int[] list = {2,3,1,-4,-7,9,5,12,0};
mergeSort(list);
for (int j : list) {
System.out.print(j + " ");
}
}
}
/*
-7 -4 0 1 2 3 5 9 12
*/
其中方法merge的归并实现主要是靠下标跟踪,两数组元素逐一比较后,得出较小的元素放入最终数组temp中,被放入元素的所属原数组下标++(为了接着往下走)。通过两两比较最终全部放入temp中实现排序:
二、快速排序
快速排序主要是通过选出一个pivot(基准)元素,然后将数组分为两部分,使得第一部分中的所有元素都<=pivot,第二部分元素都>pivot。然后对两个部分都递归地应用快排算法。
下图总将数组的第一个元素看作pivot,然后逐一递归:
快排核心是partition方法 --> 以pivot为对比标准,用下标追踪实现元素交换 :
代码实现:
package QuitSort;
class QuickSort {
// 常规元素
public static void quickSort(int[] list) {
quickSort(list, 0, list.length - 1);
}
// 特定元素
public static void quickSort(int[] list, int first, int last) {
if (last > first) {
int pivotIndex = partition(list, first, last);
quickSort(list, first, pivotIndex - 1);
quickSort(list, pivotIndex + 1, last);
}
}
// partition方法用low和high分别指向数组两端。
public static int partition(int[] list, int first, int last) {
int pivot = list[first];
int low = first + 1; // low指向数组第二个元素
int high = last; // high指向最后一个元素
// 在数组左逐一寻找 > pivot 的元素,在右逐一寻找第一个 <= pivot 的元素。然后将他俩交换
while (high > low) {
while (low <= high && list[low] <= pivot) {
low++;
}
while (low <= high && list[high] > pivot) {
high--;
}
if (high > low) {
int temp = list[high];
list[high] = list[low];
list[low] = temp;
}
}
while (high > first && list[high] >= pivot) {
high--;
}
if (pivot > list[high]) { // 如果基准元素移动
list[first] = list[high];
list[high] = pivot;
return high; // 返回新pivot下标
} else return first; // 否则返回初始pivot下标
}
// Test
public static void main(String[] args) {
int[] list = {2, 7, 5, 66, 44, 34, -22, -1, 0, 6};
quickSort(list);
for (int i = 0; i < list.length; i++) {
System.out.print(list[i] + " ");
}
}
}
动画演示:Quick Sort Animation by Y. Daniel Liang
-
归并和快速排序的比较
- 快速排序de大量工作 --> 将两个线性表归并(排好序后进行)
- 归并排序de大量工作 --> 将线性表划分为两个子线性表(排好序前进行)
快速排序的空间效率优于归并排序,因为归并排序需要创建临时数组,快排不需要。
三、堆排序
堆排序使用二叉堆,它将所有元素先加到一个堆上,然后逐一移除最大元素来获得一个排好序的线性表。
- 二叉堆:一颗完全二叉树且每个结点 >= 它的任意一个孩子
- 完全二叉树:二叉树每一层都是满的 / 最后一层没满且最后一层叶子都靠最左放置
注意,这样的二叉树:
1、堆的存储
可以通过ArrayList或数组存储
2、添加新结点
先将其添加到堆的末尾,然后重建树(最后一个结点当作当前结点):
while(当前结点 > 父结点){
当前结点与父结点交换;
前进一层;
}
3、删除根结点
当我们删除根结点,也就是最大元素的时候,我们需要重建这颗树来保持堆的属性(根结点为当前结点):
用最后一个结点替换根结点;
while(当前结点有子结点 && 当前结点 < 子结点){
当前结点和较大子结点交换;
当前结点下退一层;
}
假设我现在要删除下面二叉堆中的62:
重建堆的过程如下(选择较大子结点交换):
接下来我们设计一个Heap堆类:
public class Heap<E extends Comparable<E>>{
// 堆在内部用数组线性表表示
private java.util.ArrayList<E> list = new java.util.ArrayList<>();
// 创建一个默认空堆
public Heap(){
}
// 创建一个具有指定对象的堆
public Heap(E[] objects){
for(int i = 0; i < objects.length; i++){
add(objects[i]);
}
}
// 添加一个新的对象到堆中
public void add(E newObject){
list.add(newObject);
int currentIndex = list.size() - 1;
while(currentIndex > 0){
int parentIndex = (currentIndex - 1) / 2;
if(list.get(currentIndex).compareTo(
list.get(parentIndex)) > 0){ // 当前结点 > 父结点
// 交换current结点和parent结点
E temp = list.get(currentIndex);
list.set(currentIndex, list.get(parentIndex)); // 使用set()替换
list.set(parentIndex,temp);
}else break;
currentIndex = parentIndex; // 让当前结点指向父结点
}
}
// 将根结点从堆中删除并返回
public E remove(){
if(list.size() == 0){
return null;
}
E removeObject = list.get(0);
list.set(0, list.get(list.size() - 1)); // 用最后一个结点替换当前根结点
list.remove(list.size() - 1); // 删掉最后一个结点
int currentIndex = 0; // 从根开始
// 确定maxIndex
while(currentIndex < list.size()){
int leftChildIndex = 2 * currentIndex + 1;
int rightChildIndex = 2 * currentIndex + 2;
if(leftChildIndex >= list.size()){
break;
}
int maxIndex = leftChildIndex;
if(rightChildIndex < list.size()){
if(list.get(maxIndex).compareTo(
list.get(rightChildIndex)) < 0){ // 比较左右谁更大
maxIndex = rightChildIndex;
}
}
if(list.get(currentIndex).compareTo(
list.get(maxIndex)) < 0) { // 当前结点 < 较大子结点
// 交换
E temp = list.get(maxIndex);
list.set(maxIndex, list.get(currentIndex));
list.set(currentIndex, temp);
currentIndex = maxIndex;
}else break;
}
return removeObject; // 每次返回最大元素
}
// 返回堆的大小
public int getSize() {
return list.size();
}
}
使用Heap类实现堆排:
public class HeapSort {
public static <E extends Comparable<E>> void heapSort(E[] list) {
Heap<E> heap = new Heap<>();
for(int i = 0; i < list.length; i++) {
heap.add(list[i]); // 添加元素到堆
}
for(int i = list.length - 1; i >= 0; i--) {
list[i] = heap.remove(); // 然后不断移除最大的根结点
}
}
public static void main(String[] args){
Integer[] list = {-44,67,89,21,100,7,5,32,2};
heapSort(list);
for(int i = 0; i < list.length; i++) {
System.out.print(list[i] + " ");
}
}
}
四、桶排序和基数排序
1、桶排序
桶排和基数排序都是对整数排序的高效算法。
桶排序实现思路是将对应的数字放到下标为自身值的“桶”中。
import java.util.ArrayList;
import java.util.Collections;
public class bucketSort {
public static void bucketSort(int[] arr) {
// 逐一找出最大元素和最小元素
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]); // 返回二者较大元素
min = Math.min(min, arr[i]); // 返回二者较小元素
}
//桶数
int bucketNum = (max - min) / arr.length + 1;
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
for (int i = 0; i < bucketNum; i++) {
bucketArr.add(new ArrayList<Integer>());
}
//将每个元素放入桶
for (int i = 0; i < arr.length; i++) {
int num = (arr[i] - min) / (arr.length);
bucketArr.get(num).add(arr[i]);
}
//对每个桶进行排序
for (int i = 0; i < bucketArr.size(); i++) {
Collections.sort(bucketArr.get(i));
}
System.out.println(bucketArr.toString());
}
public static void bucketSort(int[] a, int max) {
int[] buckets;
if (a==null || max<1)
return ;
// 创建一个容量为max的数组buckets,并且将buckets中的所有数据都初始化为0。
buckets = new int[max];
// 1. 计数
for(int i = 0; i < a.length; i++)
buckets[a[i]]++;
// 2. 排序
for (int i = 0, j = 0; i < max; i++) {
while( (buckets[i]--) >0 ) {
a[j++] = i;
}
}
buckets = null;
}
public static void main(String[] args) {
int[] list = {1, 0, 2, 2, 6, 4, 2, 3, -1, -4, -1};
bucketSort(list);
}
}
2、基数排序
虽然桶排序比较稳定,但当键值范围过大时,桶排序就不是很可取。基数排序是基于桶排序的,但它只有10个桶。
基数排序的原理是,按照位次逐一对元素的位进行桶排序(0~9):
代码实现如下:
public class RadixSort {
// 获取数组a中最大值
private static int getMax(int[] a) {
int max;
max = a[0];
for (int i = 1; i < a.length; i++) {
if (a[i] > max){
max = a[i];
}
}
return max;
}
// 对数组按照"某个位数"进行排序(桶排序)
private static void countSort(int[] a, int exp) {
int[] output = new int[a.length]; // 存储"被排序数据"的临时数组
int[] buckets = new int[10];
// 将数据出现的次数存储在buckets[]中
for (int i = 0; i < a.length; i++) {
buckets[(a[i] / exp) % 10]++;
}
// 让更改后的buckets[i]的值是该数据在output[]中的位置
for (int i = 1; i < 10; i++) {
buckets[i] += buckets[i - 1];
}
// 将数据存储到临时数组output[]中
for (int i = a.length - 1; i >= 0; i--) {
output[buckets[ (a[i]/exp)%10 ] - 1] = a[i];
buckets[ (a[i]/exp)%10 ]--;
}
// 将排序好的数据赋值给a[]
for (int i = 0; i < a.length; i++) {
a[i] = output[i];
}
output = null;
buckets = null;
}
// 基数排序
public static void radixSort(int[] a) {
int exp; // 指数。当对数组按各位进行排序时,exp=1;按十位进行排序时,exp=10
int max = getMax(a); // 数组a中的最大值
// 从个位开始,对数组a按"指数"进行排序
for (exp = 1; max/exp > 0; exp *= 10)
countSort(a, exp);
}
public static void main(String[] args) {
int i;
int a[] = {53, 3, 542, 748, 14, 214, 154, 63, 616};
radixSort(a); // 基数排序
for (i=0; i<a.length; i++) {
System.out.printf("%d ", a[i]);
}
}
}
五、外部排序
如果对一些大型外部文件排序,首先我们需要将这些数据放入内存中,然后进行排序。但如果咱的文件太大、内存不足以一下子存储文件中所有数据的时候,我们就可以选择外部排序算法。
外部排序算法用到了IO流一块的类,参考:Java(8)二进制IO_颜 然的博客-CSDN博客_java二进制io
首先我们先创建一个包含大量数据的文件:
import java.io.*;
public class CreateLargeFile {
public static void main(String[] args) throws Exception {
DataOutputStream output = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream("largedata.dat")));
for(int i = 0; i < 2_000_000; i++) // 放入20万个int值数据
output.writeInt((int)(Math.random() * 1000000));
output.close();
// 先展示100个
DataInputStream input = new DataInputStream(
new BufferedInputStream(new FileInputStream("largedata.dat")));
for(int i = 0; i < 100; i++){
System.out.print(input.readInt() + " ");
}
input.close();
}
}
外部排序结合归并排序,大致分为两个阶段 --> 分段排序 --> 迭代归并:
1、分段排序
重复将数据从文件读入数组中,然后用内部排序sort方法对数组排序,然后将数组输入到一个临时文件中:
// 创建初始有序分段
public static int initializeSegments(int segmentSize, String originalFile, String f1) throws Exception {
int[] list = new int[segmentSize]; // 创建具有最大尺寸的数组
// 为原始文件创建一个数据输入流
DataInputStream input = new DataInputStream(
new BufferedInputStream(new FileInputStream(originalFile)));
// 为原始文件创建一个数据输出流
DataOutputStream output = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream(f1)));
int numberOfSegments = 0;
while(input.available() > 0){
numberOfSegments++;
int i = 0;
// 从文件中读取一段数据到数组中
for( ; input.available() > 0 && i < segmentSize; i++){
list[i] = input.readInt();
}
java.util.Arrays.sort(list, 0 , 1); // 排序
for(int j = 0; i < i; j++){
output.writeInt(list[j]); // 将数组数据写入临时文件中
}
}
input.close();
output.close();
return numberOfSegments; // 返回分段个数
}
2、迭代归并
将每对有序分段归并到一个更大的有序分段中(二合一,每次归并后分段个数少一半),并将新分段存储到新的临时文件中。然后继续进行迭代归并直到最后只有一个分段结果:
具体操作如下图。先将f1中的一半分段先复制到临时文件f2中,然后对f1和f2中的数据进行归并合成到f3中:
代码部分如下:
(1)复制f1中前半部分至f2
private static void copyHalfTof2(int numberOfSegments, int segmentSize, DataInputStream f1, DataOutputStream f2) throws Exception{
for(int i = 0; i < (numberOfSegments / 2) * segmentSize; i++){
f2.writeInt(f1.readInt()); // 将一个 int 值作为四个字节写入底层流
}
}
(2)归并所有分段
private static void mergeSegments(int numberOfSegments,int segmentSize, DataInputStream f1, DataInputStream f2, DataOutputStream f3) throws Exception{
for(int i = 0; i < numberOfSegments; i++){
mergeTwoSegments(segmentSize, f1, f2, f3); // 归并两个片段
}
while (f1.available() > 0){
f3.writeInt(f1.readInt());
}
}
(3)归并两个片段
private static void mergeTwoSegments(int segmentSize, DataInputStream f1, DataInputStream f2, DataOutputStream f3) throws Exception{
int intFormF1 = f1.readInt();
int intFormF2 = f2.readInt();
int f1Count = 1;
int f2Count = 1;
// available() 方法可以在读写操作前先得知数据流里有多少个字节可以读取
while(true){
// 判断谁的字节数少,就从谁先开始
if(intFormF1 < intFormF2){ // f1少
f3.writeInt(intFormF1);
if(f1.available() == 0 || f1Count++ >= segmentSize){
f3.writeInt(intFormF2);
break;
}else {
intFormF1 = f1.readInt();
}
}else{ // f2少
f3.writeInt(intFormF2);
if(f2.available() == 0 || f2Count++ >= segmentSize){
f3.writeInt(intFormF1);
break;
}else{
intFormF2 = f2.readInt();
}
}
}
// 如果还有多出的数据,直接写入f3
while(f1.available() > 0 && f1Count++ < segmentSize){
f3.writeInt(f1.readInt());
}
while(f2.available() > 0 && f2Count++ < segmentSize){
f3.writeInt(f2.readInt());
}
}
3、二合一的全部实现
import java.io.*;
public class SortLargeFile {
public static final int MAX_ARRAY_SIZE = 100000;
public static final int BUFFER_SIZE = 100000;
public static void main(String[] args) throws Exception {
// 从largedata.dat中读取数据,排好序后写入sortedfile.dat中
sort("largedata.dat", "sortedfile.dat");
// 显示指定文件的前100个数据
displayFile("sortedfile.dat");
}
/**
* 排序
*/
public static void sort(String sourcefile, String targetfile) throws Exception{
// 创建初始数据,然后将排好序的分段存入f1.dat
int numberOfSegments = initializeSegments(MAX_ARRAY_SIZE, sourcefile, "f1.dat");
// 在targetfile中产生一个有序文件
merge(numberOfSegments,MAX_ARRAY_SIZE,"f1.dat","f2.dat","f3.dat",targetfile);
}
/**
* 1、分段排序
* 创建初始有序分段
*/
public static int initializeSegments(int segmentSize, String originalFile, String f1) throws Exception {
int[] list = new int[segmentSize]; // 创建具有最大尺寸的数组
// 为原始文件创建一个数据输入流
DataInputStream input = new DataInputStream(
new BufferedInputStream(new FileInputStream(originalFile)));
// 为原始文件创建一个数据输出流
DataOutputStream output = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream(f1)));
int numberOfSegments = 0;
while(input.available() > 0){
numberOfSegments++;
int i = 0;
// 从文件中读取一段数据到数组中
for( ; input.available() > 0 && i < segmentSize; i++){
list[i] = input.readInt();
}
java.util.Arrays.sort(list, 0 , 1); // 排序
for(int j = 0; i < i; j++){
output.writeInt(list[j]); // 将数组数据写入临时文件中
}
}
input.close();
output.close();
return numberOfSegments; // 返回分段个数
}
/**
* 归并函数
*/
// f2辅助f1归并到f3
public static void merge(int numberOfSegments,int segmentSize,String f1,String f2,String f3,String targetfile) throws Exception{
if(numberOfSegments > 1){
mergeOneStep(numberOfSegments,segmentSize,f1,f2,f3);
// 调用新归并(每归并一次,numberOfSegments减半,有序分段大小segmentSize大小翻倍)
merge((numberOfSegments + 1) / 2, segmentSize * 2, f3, f1, f2,targetfile);
}else { // numberOfSegment = 1时,结束merge递归
File sortedFile = new File(targetfile);
if(sortedFile.exists()){
sortedFile.delete();
}
// 这时排好的数据在f1中
new File(f1).renameTo(sortedFile); // 重命名为sortedFile
}
}
/**
* 2、迭代归并
*/
public static void mergeOneStep(int numberOfSegments, int segmentSize, String f1,String f2,String f3) throws Exception{
DataInputStream f1Input = new DataInputStream(
new BufferedInputStream(new FileInputStream(f1), BUFFER_SIZE));
DataOutputStream f2Output = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream(f2), BUFFER_SIZE));
// 从f1拷贝一半的段到f2
copyHalfTof2(numberOfSegments,segmentSize,f1Input,f2Output);
f2Output.close();
// 将f1中剩余的数据段与f2中的数据段一起合并到f3中
DataInputStream f2Input = new DataInputStream(new BufferedInputStream(new FileInputStream(f2),BUFFER_SIZE));
DataOutputStream f3Output = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(f3), BUFFER_SIZE));
mergeSegments(numberOfSegments / 2, segmentSize, f1Input,f2Input,f3Output);
f1Input.close();
f2Input.close();
f3Output.close();
}
/**
* 复制f1中前半部分至f2
*/
private static void copyHalfTof2(int numberOfSegments, int segmentSize, DataInputStream f1, DataOutputStream f2) throws Exception{
for(int i = 0; i < (numberOfSegments / 2) * segmentSize; i++){
f2.writeInt(f1.readInt()); // 将一个 int 值作为四个字节写入底层流
}
}
/**
* 归并所有分段
*/
private static void mergeSegments(int numberOfSegments,int segmentSize, DataInputStream f1, DataInputStream f2, DataOutputStream f3) throws Exception{
for(int i = 0; i < numberOfSegments; i++){
mergeTwoSegments(segmentSize, f1, f2, f3); // 归并两个片段
}
while (f1.available() > 0){
f3.writeInt(f1.readInt());
}
}
/**
* 归并两个片段
*/
private static void mergeTwoSegments(int segmentSize, DataInputStream f1, DataInputStream f2, DataOutputStream f3) throws Exception{
int intFormF1 = f1.readInt();
int intFormF2 = f2.readInt();
int f1Count = 1;
int f2Count = 1;
// available() 方法可以在读写操作前先得知数据流里有多少个字节可以读取
while(true){
// 判断谁的字节数少,就从谁先开始
if(intFormF1 < intFormF2){ // f1少
f3.writeInt(intFormF1);
if(f1.available() == 0 || f1Count++ >= segmentSize){
f3.writeInt(intFormF2);
break;
}else {
intFormF1 = f1.readInt();
}
}else{ // f2少
f3.writeInt(intFormF2);
if(f2.available() == 0 || f2Count++ >= segmentSize){
f3.writeInt(intFormF1);
break;
}else{
intFormF2 = f2.readInt();
}
}
}
// 如果还有多出的数据,直接写入f3
while(f1.available() > 0 && f1Count++ < segmentSize){
f3.writeInt(f1.readInt());
}
while(f2.available() > 0 && f2Count++ < segmentSize){
f3.writeInt(f2.readInt());
}
}
/**
* 显示指定文件的前100个数据
*/
public static void displayFile(String filename){
try{
DataInputStream input = new DataInputStream(new FileInputStream(filename));
for(int i = 0; i < 100; i++) {
System.out.print(input.readInt() + " ");
}
input.close();
} catch(IOException ex) {
ex.printStackTrace();
}
}
}