1 插入排序
思路:
(1) 将数组划分为 “ 已排序区间 ” 和 “ 待排序区间 ”,用 bound 来标识这两个区间,bound 为待排序区间的第一个位置;
(2) 将待排区间的第一个元素(即bound 标记位置的元素),与已排区间的元素进行比较。
public static void insertSort(int[] arr) {
// 划分“已排区间” 和 “待排区间”
int bound = 1;
// 用于控制边界值向后走
// 即,待排区间往后缩小
for (; bound < arr.length; bound ++) {
int v = arr[bound];
// 待排区间的前一个元素
int cur = bound - 1;
// 已排区间从后往前遍历
// 寻找合适的位置插入 arr[bound]
for (; cur >= 0; cur --) {
if (arr[cur] > v) {
// 元素向后移动一位
arr[cur + 1] = arr[cur];
} else {
break;
}
}
// 找到合适位置,插入
arr[cur + 1] = v;
}
}
时间复杂度:O( N2 )
空间复杂度:O(1)
稳定排序
2 希尔排序
思路:
规定一个间隔(间隔希尔序列,length / 2,length / 4,length / 8 … 1),将数组分成几个组,对每一组分别进行排序,然后调整分组变化,逐渐使数组变得有序。可以构建两个方法,第一个方法构建分组系数,第二个方法以前的 gap 为依据,进行分组插排。
希尔排序是插入排序的升级版本,因此它的代码实现和思想与插入排序很类似。
public static void shellSort(int[] arr) {
int gap = arr.length / 2;
while (gap >= 1) {
_shellSort(arr,gap);
gap = gap / 2;
}
}
public static void _shellSort(int[] arr,int gap) {
int bound = gap;
for (; bound < arr.length ; bound++) {
int v = arr[bound];
int cur = bound - gap;
for (; cur >= 0 ; cur = cur - gap) {
if (arr[cur] > v) {
arr[cur + gap] = arr[cur];
} else {
break;
}
}
arr[cur + gap] = v;
}
}
时间复杂度:O(N2) => O(N1.3)
空间复杂度:O(1)
不稳定排序
3 选择排序
(1) 将数组分为两个区间,待排区间和已排区间,但注意,这里与插入排序不同的是,插入排序 bound 可以是任意一个元素,而选择排序的 bound 必须从 0 开始。 因为选择排序在后续进行选择时,会选出最小值放到已排区间的最后,如果一开始随意选择,后续的元素不一定都比这个元素大,要确保第一个选择的元素是数组最小的元素;
(2) 遍历待排区间,通过打擂台的方式找出最小元素,以待排区间的第一个位置作为擂台。
public static void selectOrder(int[] arr) {
int bound = 0;
for (;bound < arr.length; bound ++) {
for (int cur = bound + 1; cur < arr.length; cur++) {
if (arr[cur] < arr[bound]) {
swap(arr,cur,bound);
}
}
}
}
public static void swap (int[] arr,int cur,int bound) {
int tmp = arr[cur];
arr[bound] = arr[cur];
arr[cur] = tmp;
}
时间复杂度:O(N2)
空间复杂度:O(1)
不稳定排序
4 堆排序
(1) 用当前数组创建一个大堆;
(2) 删除堆顶元素,即 将堆顶的元素和最后的元素进行交换,然后从堆中删除最后一个元素,注意并非从数组中删除,最大值就被放在了数组最后;
(3) 从 0 号元素进行向下调整,使其重新成为堆;
(4) 再把堆的第一个元素和最后一个元素进行交换,此时的最后一个元素是数组的倒数第二个元素,就把第二大的数放在了数组的倒数第二个位置,以此类推,不断循环。
public static void heapSort(int[] arr) {
createHeap(arr);
int heapSize = arr.length;
for (int i = 0; i < arr.length; i++) {
// 交换堆上的 第一个和最后一个元素
// 注意堆的最后一个元素并不是每次都是数组的最后一个元素
swap(arr,0,heapSize - 1);
// 删除堆的最后一个元素,但是它还存在在数组中
heapSize --;
// 只用调整一次,因为其本身已经是堆,只是交换后的第一个元素需要调整
shiftDown(arr,heapSize,0);
}
}
private static void createHeap(int[] arr) {
for (int i = ((arr.length - 1) - 1 ) / 2; i >= 0 ; i--) {
shiftDown(arr,arr.length,i);
}
}
public static void shiftDown(int[] arr,int size,int index) {
int parent = index;
int child = 2 * parent + 1;
while (child < size) {
if (child + 1 < size && arr[child + 1] > arr[child]) {
child = child + 1;
}
if (arr[child] > arr[parent]) {
swap(arr,child,parent);
}
parent = child;
child = 2 * parent + 1;
}
}
时间复杂度:O( NlogN)
空间复杂度:O(1)
不稳定排序
5 冒泡排序
思路:
比较两个相邻的元素,不符合升序,就交换,如果从后往前遍历,则一趟遍历之后,最大值来到最后;如果从前往后遍历,则一趟遍历之后,最小值来到最后。
public static void bubbleSort(int[] arr) {
for (int bound = 0; bound < arr.length; bound++) {
for (int cur = arr.length - 1; cur > bound ; cur--) {
if (arr[cur - 1] > arr[cur]) {
swap(arr,cur - 1,cur);
}
}
}
}
时间复杂度:O(N2)
空间复杂度:O(1)
稳定排序
6 快速排序
思路:
(1) 在待排数组中选取一个 “基准值”,然后把这个数组整理为:左侧都比基准值小,右侧都比基准值大;
(2) 使用左右下标从两边往中间遍历。
public static void quickSort(int[] arr) {
_quickSort(arr,0,arr.length - 1);
}
public static void _quickSort(int[] arr,int left,int right) {
if (left >= right) {
return;
}
// index 表示基准值所在的位置
int index = partition(arr,left,right);
_quickSort(arr,left,index - 1);
_quickSort(arr,index + 1,right);
}
public static int partition(int[] arr,int left,int right) {
// v 用来存放基准值
int v = arr[right];
int l = left;
int r = right;
while (l < r) {
// 找到大于基准值的元素的位置
while (l < r && arr[l] <= v) {
l ++;
}
// 找到小于基准值的元素的位置
while (l < r && arr[r] >= v) {
r --;
}
// 交换两个位置的元素
swap(arr,l,r);
}
// 当 l == r 时,循环进不去
// 交换 l位置 和 right位置的元素,right 为基准值的位置
swap(arr,l,right);
return l;
}
时间复杂度:O( NlogN) 最坏为 O(N)
空间复杂度:O(logN)最坏为 O(N) 取决于递归的深度
不稳定排序
快速排序的优化手段:
1、三数取中:即,取三个数,数组的第一个数、最后一个数和中间的数,哪个是中间的数就取哪个;
2、当待处理区间已经比较小的时候,就不继续递归了,直接针对该区间进行插入排序;
3、当递归深度达到一定深度,并且当前待处理区间还是比较大时,还可以使用堆排序