1.堆排序(Heapsort)
1.1算法描述
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。是一种不稳定的排序算法。
排序过程描述:
1.将待排序数组(R1,R2….Rn)构建成大根堆,此堆为初始的无序区
2.将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
3.由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)向下调整为新的大根堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
动图演示:
1.2算法实现
import java.util.Arrays;
public class HeapSort {
public static void swap(int[] arr, int a, int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
public static void shiftDownBig(int[] arr, int length, int parent){// 向下调整
int child = parent * 2 + 1;
while(child < length ){
if(child + 1 < length && arr[child + 1] > arr[child]){
child++;
}
if(arr[child] > arr[parent]){
swap(arr, child, parent);
parent = child;
child = parent * 2 + 1;
}else{
break;
}
}
}
public static void heapSort(int[] arr){//堆排序
int length = arr.length;
for(int i = (length - 2) / 2; i >= 0; i--){//建堆
shiftDownBig(arr, length, i);
}
int end = length - 1;//最后一个下标
while(end > 0){//一直调整到数组第一个
swap(arr, 0, end);//交换堆顶和未排序范围最后一个(堆顶是最大的,这样就放在了未排序的最后一个,完成这个元素的排序)
shiftDownBig(arr, end, 0);//将堆顶换上去的元素在未排序范围内向下调整
end--;
}
}
public static void main(String[] args) {
int[] a = {24, 63, 15, 33, 51, 35, 76, 33};
heapSort(a);
System.out.println(Arrays.toString(a));
}
}
1.3复杂度分析
最佳时间复杂度:O(nlogn)
最坏时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
空间复杂度:O(1)
2.快速排序(Quicksort)
2.1算法描述
快速排序就是先找一个基准值,将待排序序列分为两部分,一部分中所有数字大于基准值,另一部分小于基准值,再对两部分分别进行排序,直到整个序列有序。是一种不稳定的排序算法。
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。
具体算法过程如下:
1.从数列中挑出一个元素,称为 “基准”(pivot);
2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
分区具体过程:
- 首先选择此分区最左边的元素(也可三数取中法)作为基准值,记录此基准值的位置
- 然后从此分区最右边开始遍历找到第一个比基准值小的元素,再从左边找第一个大于基准值的元素,交换两个元素
- 再次从刚才的右边位置向左找第一个比基准值小的元素,再从刚才的左边位置找第一个大于基准值的元素,交换两个元素
- 重复步骤4,直到左右相遇
- 将基准值位置(最左边开始的位置)与相遇点元素交换
这样就将比基准值小全部划分到基准值左边分区,比基准值大的全部划分到基准值右边分区
3.递归地(recursive)把分出的两个分区再次进行排序。
2.2算法实现
1)递归实现:
import java.util.Arrays;
public class QuickSort {
public static void swap(int[] arr, int a, int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
//快速排序一次排序过程
public static int quick(int[] arr, int left, int right){
//保留基准值
int value = arr[left];
//基准值所在下表
int start = left;
//只要左指针和右指针没有相遇进行循环
while(left < right){
//从右向左找第一个小于基准值的位置(这里也必须保证left < right),
// 注意这里必须先从右边找第一个小于基准值的数,如果先从左边找的话会产生错误
while(left < right && arr[right] >= value){
right--;
}
//从左到右找第一个大于基准值的数位置
while(left < right && arr[left] <= value){
left++;
}
//找到之后,交换两个数
swap(arr, left, right);
}
//遍历完整个数组后,交换基准值和两个指针相遇位置的值
swap(arr, start, left);
return left;
}
//快速排序
public static void quickSort(int[] arr, int left, int right){
if(left < right){
//取出排序相遇位置的坐标
int mid = quick(arr, left, right);
//从上次相遇位置进行分组,对左右组分别进行快速排序
quickSort(arr, left, mid - 1);
quickSort(arr, mid + 1, right);
}
}
public static void main(String[] args) {
int[] a = {24, 63, 15, 33, 51, 35, 76, 33};
quickSort(a, 0, a.length - 1);
System.out.println(Arrays.toString(a));
}
}
2)非递归实现:
//快速排序(非递归实现)
import java.util.Arrays;
import java.util.Stack;
public class QuickSort {
public static void swap(int[] arr, int left, int right){
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
//双指针进行一次排序过程
public static int quick(int[] arr, int left, int right){
int value = arr[left];
int prev = left;//慢指针
int cur = left + 1;//快指针
while(cur <= right){
if(arr[cur] < value && prev++ != cur){//找到小于基准值的位置,如果prev走向下一个位置不是cur的位置,说明此时prev指向的值大于基准值
swap(arr, prev, cur);
}
cur++;
}
swap(arr, left, prev);
return prev;
}
//非递归快速排序
public static int[] quickSort(int[] arr, int left, int right){
Stack<Integer> stack = new Stack<>();
if(left < right){//先将数组左右下标压入栈中(注意这里你的压栈顺序,等会取出时要先取后压入的)
stack.push(left);//先压左
stack.push(right);//右
}
while(!stack.isEmpty()){
//每次取出两个元素,作为要排序区间的左右范围
int right1 = stack.pop();//先取右
int left1 = stack.pop();//左
int mid = quick(arr, left1, right1);//对当前区间进行排序,并记录中间位置的下标
if(mid - 1 > left1){//如果中间位置向左的区间元素数量大于1,将左区间的左右下标压入栈
stack.push(left1);//先压左
stack.push(mid - 1);//右
}
if(mid + 1 < right1){//同理
stack.push(mid + 1);
stack.push(right1);
}
}
return arr;
}
public static void main(String[] args) {
int[] a = {24, 63, 15, 33, 51, 35, 76, 33};
System.out.println(Arrays.toString(quickSort(a, 0, a.length - 1)));
}
}
2.3复杂度分析
最佳时间复杂度:O(nlogn)
最坏时间复杂度:O(n^2)
平均时间复杂度:O(nlogn)
最佳空间复杂度:O(logn)
最坏空间复杂度:O(n)
平均空间复杂度:O(logn)
3.归并排序
3.1算法描述
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。归并排序是一种稳定的排序算法
具体算法过程如下:
整个过程分为划分和归并两个过程
划分过程:
1.把长度为n的输入序列分成两个长度为n/2的子序列;
2.把长度为n/2的输入序列分成两个长度为n/4的子序列;
3.直到将输入序列划分成一个个长度为1的子序列
归并过程:
1.将两个长度为1的子序列进行归并(也就是将长度为1的子序列合并成长度为2的子序列,合并时排序)
2.接着在上一步的基础上归并长度为2的数组,直到全部归并完毕
动图演示:
3.2算法实现
1)递归实现
//归并排序实现
import java.util.Arrays;
public class MergeSort {
//合并一次数组
public static void merge(int[] arr, int left, int mid, int right, int[] temp){
int start1 = left, end1 = mid;//合并的一个数组的左右区间
int start2 = mid + 1, end2 = right;//合并的另一个数组的左右区间
int index = left;//记录合并数组最开始的下标
while(start1 <= mid && start2 <= end2){//合并
if(arr[start1] <= arr[start2]){//先存小的
temp[index++] = arr[start1++];
}else{
temp[index++] = arr[start2++];
}
}
//判断是否还有剩余元素,如果有将剩余元素添加
while(start1 <= end1){
temp[index++] = arr[start1++];
}
while(start2 <= end2){
temp[index++] = arr[start2++];
}
for(int i = left; i <= right; i++){//拷贝
arr[i] = temp[i];
}
}
//进行递归归并排序
public static void mSort(int[] arr, int left, int right, int[] temp){
if(left >= right){
return;
}
int mid = (left + right) / 2;//中间坐标
mSort(arr, left, mid, temp);//左边数组进行排序
mSort(arr, mid + 1, right, temp);//右边数组进行排序
merge(arr, left, mid, right, temp);//进行合并
}
//归并排序
public static int[] mergeSort(int[] arr){
int[] temp = new int[arr.length];
mSort(arr, 0, arr.length - 1, temp);
return arr;
}
public static void main(String[] args) {
int[] a = {24, 63, 15, 33, 51, 35, 76, 33};
System.out.println(Arrays.toString(mergeSort(a)));
}
}
2)非递归实现
import java.util.Arrays;
public class MergeSort {
//合并一次数组
public static void merge(int[] arr, int left, int mid, int right, int[] temp){
int start1 = left, end1 = mid;//合并的一个数组的左右区间
int start2 = mid + 1, end2 = right;//合并的另一个数组的左右区间
int index = left;//记录合并数组最开始的下标
while(start1 <= mid && start2 <= end2){//合并
if(arr[start1] <= arr[start2]){//先存小的
temp[index++] = arr[start1++];
}else{
temp[index++] = arr[start2++];
}
}
//判断是否还有剩余元素,如果有将剩余元素添加
while(start1 <= end1){
temp[index++] = arr[start1++];
}
while(start2 <= end2){
temp[index++] = arr[start2++];
}
for(int i = left; i <= right; i++){//拷贝
arr[i] = temp[i];
}
}
public static int[] mergeSort(int[] arr){
int[] temp = new int[arr.length];
for(int i = 1; i < arr.length; i *= 2){//外层循环i是每次归并的元素个数
for(int j = 0; j < arr.length; j += i * 2){//内层循环j是下次归并的起始位置下标
int left = j;
int mid = left + i - 1;
if(mid >= arr.length - 1){//如果右半部分没有数据,不用进行归并
continue;
}
int right = left + 2 * i - 1;
if(right > arr.length - 1){//如果右区间下标越界
right = arr.length - 1;
}
merge(arr, left, mid, right, temp);
}
}
return arr;
}
public static void main(String[] args) {
int[] a = {24, 63, 15, 33, 51, 35, 76, 33};
System.out.println(Arrays.toString(mergeSort(a)));
}
}
3.3复杂度分析
最佳时间复杂度:O(nlogn)
最坏时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
空间复杂度:O(n)