内部排序(升序为例)
1. 冒泡排序
每次可以确定出一个元素的最终位置。
public static void bubbleSort(int[] arr){
int len = arr.length;
for(int i = 0; i < len; i++){
for(int j = 0; j < len - i - 1; j++){
if(arr[j] > arr[j + 1]){
swap(arr, j, j + 1);
}
}
}
}
//交换函数
private static void swap(int[] arr, int j, int i) {
int tmp = arr[j];
arr[j] = arr[i];
arr[i] = tmp;
}
时间复杂度O(n^2),空间复杂度O(1),是稳定的排序算法。但是当数组已经有序的时候,这样显然是没有比较的必要,修改如下:
public static void bubbleSort(int[] arr){
int len = arr.length;
for(int i = 0; i < len; i++){
boolean flag = true;
for(int j = 0; j < len - i - 1; j++){
if(arr[j] > arr[j + 1]){
swap(arr, j, j + 1);
flag = false;
}
}
if(flag == true){
break;
}
}
}
增加一个标志flag,如果一次都没有交换过,flag == true,直接跳出循环,此时时间复杂度为O(n),空间复杂度为O(1)。
2. 快速排序
快速排序的基本思路是:(1)确定要排序的数组区间[left, right];(2)找到一个基准值pivotIndex(三种方法);(3)遍历整个区间,并将区间分为三部分,比基准值小的区间[left, povitIndex - 1],比基准值大的区间[povitIndex + 1, right];(4)分治。
选择基准值的三种方法:(1)最边上的数,即数组最左边arr[left]或者数组最右边arr[right]的数,但是当数组已经有序或者逆序时,这样选择基准值,都是最坏的情况;(2)随机法:在数组中随机选择一个数;(3)三数取中法:如9,8,7,6,5,就在9,7,5中选择7作为基准值,然后将基准值交换到边上就行。
关于第三步中,如何实现比基准值小的数在区间[left, povitIndex - 1]中,比基准值大的数在区间[povitIndex + 1, right]中,有三种方法,分别是:(1)Hover法;(2)挖坑法;(3)前后下标法。具体实现看代码:此处基准值均选择最右边的值arr[right],所以应该先走左边begin,反之先走右边right,即基准值与开始走的位置相反,否则会出现数组中有些数不会被比较的情况。
public static void quickSort(int[] arr){
int len = arr.length;
quickSortInner(arr, 0, len - 1);
}
private static void quickSortInner(int[] arr, int left, int right) {
//区间中没有数或者区间不存在
if(left >= right){
return ;
}
//如何确保小数在右,大数在左,三种方法
int pivotIndex = parition(arr, left, right);
//分治,两个小区间,一个存放大于基准值的数,一个存放小于基准值的数
quickSortInner(arr, left, pivotIndex - 1);
quickSortInner(arr, pivotIndex + 1, right);
}
//Hover法
private static int parition(int[] arr, int left, int right) {
int begin = left;
int end = right;
//选择最右边的数arr[right]作为基准值
int pivot = arr[right];
while(begin < end){
while(begin < end && arr[begin] <= pivot){
begin++;
}
while(begin < end && arr[end] >= pivot){
end--;
}
//不满足条件,交换begin和end的值
swap(arr, begin, end);
}
//最后将right与begin指向的值交换
swap(arr,begin,right);
//begin所指向的数就是基准值
return begin;
}
private static void swap(int[] arr, int begin, int end) {
int tmp = arr[begin];
arr[begin] = arr[end];
arr[end] = tmp;
}
//挖坑法
private static int parition1(int[] arr, int left, int right) {
int begin = left;
int end = right;
//选择右边的数作为基准值,即最右边有一个坑
int pivot = arr[right];
while(begin < end){
while(begin < end && arr[begin] <= pivot){
begin++;
}
//当不满足条件时,把begin的值赋给end填坑,给begin处挖了一个坑
arr[end] = arr[begin];
while(begin < end && arr[end] >= pivot){
end--;
}
arr[begin] = arr[end];
}
//最终基准值赋给begin处
arr[begin] = pivot;
return begin;
}
//前后指针法
private static int parition2(int[] arr, int left, int right) {
//tmp后指针
int tmp = left;
//i前指针
for(int i = left; i < right; i++){
//相当于把arr[right]作为基准值
if(arr[i] < arr[right]){
//交换使得小于基准值的数在前,大于基准值的数在后
swap(arr, i, tmp);
tmp++;
}
}
//最后交换,得到基准值tmp
swap(arr, tmp, right);
return tmp;
}
快排时间复杂度O(n*log(n)),空间复杂度O(log(n)),是一种不稳定的排序算法。
3. 直接插入排序
基本思想是:i之前的区间已经有序,需要找到i对应元素tmp的正确位置,与前面的数作比较并将比较过的元素后移一位,直至找到正确的位置并赋值。
public static void insertSort(int[] arr){
//有序区间[0, i),无序区间[i, len)
int len = arr.length;
for(int i = 0; i < len; i++){
//tmp为将要移动的元素
int tmp = arr[i];
int j = i - 1;
//i之前都是有序的,若tmp < arr[j],将比较过的元素后移一位
for(; j >= 0 && tmp < arr[j]; j--){
arr[j + 1] = arr[j];
}
//找到tmp的正确位置并赋值
arr[j + 1] = tmp;
}
}
时间复杂度为O(n^2),空间复杂度为O(1),是稳定的排序算法。
4. 希尔排序
希尔排序是对直接插入排序的一种改进(分组插排),先进行一次预排序,很快让数据变得基本有序;然后进行插排。
public static void shellSort(int[] arr){
int gap = arr.length;
while(true){
gap = (gap / 3) + 1;
insertSortWithGap(arr, gap);
//当步长 == 1时,说明整个区间已经有序
if(gap == 1){
break;
}
}
}
private static void insertSortWithGap(int[] arr, int gap) {
int len = arr.length;
for(int i = 0; i < len; i++){
int tmp = arr[i];
int j = i - gap;
for(; j >= 0 && tmp < arr[j]; j = j - gap){
arr[j + gap] = arr[j];
}
arr[j + gap] = tmp;
}
}
空间复杂度为O(1),是不稳定的排序算法。
5. 简单选择排序
基本思想是:在无序区间中找到最大数(或最小的数),放在无序区间的后面,每循环一次,记录最大数的位置,并不直接交换,而是最后进行数据的交换,直至整个区间有序。
public static void selectSort(int[] arr){
int len = arr.length;
//每次选择最大的数放在无序区间的最后
//[0, len - i)是无序区间,[len - i, len)是有序区间
for(int i = 0; i < len; i++){
//假设最大数下标为0
int max = 0;
//在无序区间中找出最大数
for(int j = 1; j < len - i; j++){
if(arr[max] < arr[j]){
max = j;
}
}
//并将该最大数放到区间末尾
int tmp = arr[max];
arr[max] = arr[len - i - 1];
arr[len - i - 1] = tmp;
}
}
时间复杂度为O(n^2),空间复杂度为O(1),是稳定的排序算法。
6. 堆排序
public static void heapSort(int[] arr){
int len = arr.length;
//建堆过程
for(int i = (len - 2) / 2; i >= 0; i--){
bigHeapify(arr, len, i);
}
//选择排序
for(int j = 0; j < len; j++){
int tmp = arr[0];
arr[0] = arr[len - j - 1];
arr[len - j - 1] = tmp;
bigHeapify(arr, len - j - 1, 0);
}
}
//堆调整
private static void bigHeapify(int[] arr, int size, int index) {
int max = 2 * index + 1;
while(max < size){
//找最大节点
if(max + 1 < size && arr[max + 1] > arr[max]){
max += 1;
}
if(arr[index] >= arr[max]){
break;
}
int tmp = arr[index];
arr[index] = arr[max];
arr[max] = tmp;
index = max;
max = 2 * index + 1;
}
}
时间复杂度为O(nlogn),空间复杂度为O(1),是不稳定的排序算法。
7. 归并排序
//1. 平均切分
//2. 分治算法处理两个小区间
public static void mergeSortNoDiGui(int[] array){
int[] extra = new int[array.length];
for(int i = 1; i < array.length; i *= 2){
for(int j = 0; j < array.length; j += 2 * i){
int low = j;
int mid = low + i;
if(mid >= array.length){
continue;
}
int high = mid + i;
if(high > array.length){
high = array.length;
}
merge(array,low,mid,high,extra);
}
}
}
private static void merge(int[] array, int low, int mid, int high, int[] extra) {
//遍历array:[low, mid)
int i = low;
//遍历array:[mid, high)
int j = mid;
//遍历:extra
int x = 0;
while (i < mid && j < high){
//优先取左边,保证稳定性
if(array[i] <= array[j]){
extra[x++] = array[i++];
}else{
extra[x++] = array[j++];
}
}
//当一个区间的数被取完
while (i < mid){
extra[x++] = array[i++];
}
while (j < high){
extra[x++] = array[j++];
}
//从额外空间搬移出来
for(int k = low; k < high; k++){
array[k] = extra[k - low];
}
}
时间复杂度为O(nlogn),空间复杂度为O(n),是稳定的排序算法。
小结