冒泡排序
定义一个布尔变量 hasChange ,用来标记每轮是否进行了交换。在每轮遍历开始时,将 hasChange 设置为 false。
若当轮没有发生交换,说明此时数组已经按照升序排列, hashChange 依然是为 false。此时外层循环直接退出,排序结束。
private static void bubbleSort(int[] nums) {
boolean hasChange = true;
for (int i = 0, n = nums.length; i < n - 1 && hasChange; ++i) {
hasChange = false;
for (int j = 0; j < n - i - 1; ++j) {
if (nums[j] > nums[j + 1]) {
swap(nums, j, j + 1);
hasChange = true;
}
}
}
}
private static void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
public static void main(String[] args) {
int[] nums = {1, 2, 7, 9, 5, 8};
bubbleSort(nums);
System.out.println(Arrays.toString(nums));
}
算法分析
空间复杂度 O(1)、时间复杂度 O(n²)。
分情况讨论:
- 给定的数组按照顺序已经排好:只需要进行 n-1 次比较,两两交换次数为 0,时间复杂度为O(n),这是最好的情况。
- 给定的数组按照逆序排列:需要进行 n*(n-1)/2 次比较,时间复杂度为 O(n²),这是最坏的情况。
- 给定的数组杂乱无章。在这种情况下,平均时间复杂度 O(n²)。因此,时间复杂度是 O(n²),这是一种稳定的排序算法。稳定是指,两个相等的数,在排序过后,相对位置保持不变。
插入排序
先来看一个问题。一个有序的数组,我们往里面添加一个新的数据后,如何继续保持数据有序呢?很简单,我们只要遍历数组,找到数据应该插入的位置将其插入即可。
这是一个动态排序的过程,即动态地往有序集合中添加数据,我们可以通过这种方法保持集合中的数据一直有序。而对于一组静态数据,我们也可以借鉴上面讲的插入方法,来进行排序,于是就有了插入排序算法。
那么插入排序具体是如何借助上面的思想来实现排序的呢?
首先,我们将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。
与冒泡排序对比:
在冒泡排序中,经过每一轮的排序处理后,数组后端的数是排好序的。
在插入排序中,经过每一轮的排序处理后,数组前端的数是排好序的。
public static void insertionSort(int[] nums) {
for (int i = 1, j, n = nums.length; i < n; ++i) {
int num = nums[i];
for (j = i - 1; j >=0 && nums[j] > num; --j) {
nums[j + 1] = nums[j];
}
nums[j + 1] = num;
}
}
算法分析
空间复杂度 O(1),时间复杂度 O(n²)。
分情况讨论:
- 给定的数组按照顺序排好序:只需要进行 n-1 次比较,两两交换次数为 0,时间复杂度为 O(n),这是最好的情况。
- 给定的数组按照逆序排列:需要进行 n*(n-1)/2 次比较,时间复杂度为 O(n²),这是最坏的情况。
- 给定的数组杂乱无章:在这种情况下,平均时间复杂度是 O(n²)。因此,时间复杂度是 O(n²),这也是一种稳定的排序算法
选择排序
选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
public static void selectionSort(int[] nums) {
for (int i = 0, n = nums.length; i < n - 1; ++i) {
int minIndex = i;
for (int j = i; j < n; ++j) {
if (nums[j] < nums[minIndex]) {
minIndex = j;
}
}
swap(nums, minIndex, i);
}
}
public static void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
算法分析
空间复杂度 O(1),时间复杂度 O(n²)。
那选择排序是稳定的排序算法吗?
答案是否定的,选择排序是一种不稳定的排序算法。选择排序每次都要找剩余未排序元素中的最小值,并和前面的元素交换位置,这样破坏了稳定性。
比如 5,8,5,2,9 这样一组数据,使用选择排序算法来排序的话,第一次找到最小元素 2,与第一个5 交换位置,那第一个 5 和中间的 5 顺序就变了,所以就不稳定了。正是因此,相对于冒泡排序和插入排序,选择排序就稍微逊色了。
归并排序
归并排序的核心思想是分治,把一个复杂问题拆分成若干个子问题来求解。
归并排序的算法思想是:把数组从中间划分为两个子数组,一直递归地把子数组划分成更小的数组,直到子数组里面只有一个元素的时候开始排序。排序的方法就是按照大小顺序合并两个元素。接着依次按照递归的顺序返回,不断合并排好序的数组,直到把整个数组排好序。
private static void merge(int[] nums, int low, int mid, int high, int[] temp) {
int i = low, j = mid + 1, k = low;
while (k <= high) {
if (i > mid) {
temp[k++] = nums[j++];
} else if (j > high) {
temp[k++] = nums[i++];
} else if (nums[i] <= nums[j]) {
temp[k++] = nums[i++];
} else {
temp[k++] = nums[j++];
}
}
System.arraycopy(temp, low, nums, low, high - low + 1);
}
private static void mergeSort(int[] nums, int low, int high, int[] temp) {
if (low >= high) {
return;
}
int mid = (low + high) >>> 1;
mergeSort(nums, low, mid, temp);
mergeSort(nums, mid + 1, high, temp);
merge(nums, low, mid, high, temp);
}
private static void mergeSort(int[] nums) {
int n = nums.length;
int[] temp = new int[n];
mergeSort(nums, 0, n - 1, temp);
}
算法分析
空间复杂度 O(n),时间复杂度 O(nlogn)。
对于规模为 n 的问题,一共要进行 log(n) 次的切分,每一层的合并复杂度都是 O(n),所以整体时间复杂度为 O(nlogn)。由于合并 n 个元素需要分配一个大小为 n 的额外数组,所以空间复杂度为 O(n)。这是一种稳定的排序算法。
快速排序
快速排序也采用了分治的思想:把原始的数组筛选成较小和较大的两个子数组,然后递归地排序两个子数组。
private static void quickSort(int[] nums) {
quickSort(nums, 0, nums.length - 1);
}
private static void quickSort(int[] nums, int low, int high) {
if (low >= high) {
return;
}
int[] p = partition(nums, low, high);
quickSort(nums, low, p[0] - 1);
quickSort(nums, p[0] + 1, high);
}
private static int[] partition(int[] nums, int low, int high) {
int less = low - 1, more = high;
while (low < more) {
if (nums[low] < nums[high]) {
swap(nums, ++less, low++);
} else if (nums[low] > nums[high]) {
swap(nums, --more, low);
} else {
++low;
}
}
swap(nums, more, high);
return new int[]{less + 1, more};
}
private static void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
算法分析
空间复杂度 O(logn),时间复杂度 O(nlogn)。
对于规模为 n 的问题,一共要进行 log(n) 次的切分,和基准值进行 n-1 次比较,n-1 次比较的时间复杂度是 O(n),所以快速排序的时间复杂度为 O(nlogn)。
但是,如果每次在选择基准值的时候,都不幸地选择了子数组里的最大或最小值。即每次把把数组分成了两个更小长度的数组,其中一个长度为 1,另一个的长度是子数组的长度减 1。这样的算法复杂度变成 O(n²)。和归并排序不同,快速排序在每次递归的过程中,只需要开辟 O(1) 的存储空间来完成操作来实现对数组
的修改;而递归次数为 logn,所以它的整体空间复杂度完全取决于压堆栈的次数。
堆排序
堆排序是借助堆来实现的选择排序,思想同简单的选择排序,以下以大顶堆为例。注意:如果想升序排序就使用大顶堆,反之使用小顶堆。原因是堆顶元素需要交换到序列尾部。
首先,实现堆排序需要解决两个问题:
如何由一个无序序列键成一个堆?
如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?
第一个问题,可以直接使用线性数组来表示一个堆,由初始的无序序列建成一个堆就需要自底向上从第一个非叶元素开始挨个调整成一个堆。
第二个问题,怎么调整成堆?首先是将堆顶元素和最后一个元素交换。然后比较当前堆顶元素的左右孩子节点,因为除了当前的堆顶元素,左右孩子堆均满足条件,这时需要选择当前堆顶元素与左右孩子节点的较大者(大顶堆)交换,直至叶子节点。我们称这个自堆顶自叶子的调整成为筛选。
从一个无序序列建堆的过程就是一个反复筛选的过程。若将此序列看成是一个完全二叉树,则最后一个非终端节点是n/2取底个元素,由此筛选即可。举个例子
49,38,65,97,76,13,27,49序列的堆排序建初始堆和调整的过程如下
public static void heapAdjust(int[] arr, int start, int end) {
int temp = arr[start];
for (int i = 2 * start + 1; i <= end; i *= 2) {
//左右孩子的节点分别为2*i+1,2*i+2
//选择出左右孩子较小的下标
if (i < end && arr[i] < arr[i + 1]) {
i++;
}
if (temp >= arr[i]) {
break; //已经为大顶堆,=保持稳定性。
}
arr[start] = arr[i]; //将子节点上移
start = i; //下一轮筛选
}
arr[start] = temp; //插入正确的位置
}
public static void heapSort(int[] arr) {
if (arr == null || arr.length == 0)
return;
//建立大顶堆
for (int i = arr.length / 2; i >= 0; i--) {
heapAdjust(arr, i, arr.length - 1);
}
for (int i = arr.length - 1; i >= 0; i--) {
swap(arr, 0, i);
heapAdjust(arr, 0, i - 1);
}
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
希尔排序
希尔排序是插入排序的一种高效率的实现,也叫缩小增量排序。简单的插入排序中,如果待排序列是正序时,时间复杂度是O(n),如果序列是基本有序的,使用直接插入排序效率就非常高。希尔排序就利用了这个特点。基本思想是:先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序。
从上述排序过程可见,希尔排序的特点是,子序列的构成不是简单的逐段分割,而是将某个相隔某个增量的记录组成一个子序列。如上面的例子,第一堂排序时的增量为5,第二趟排序的增量为3。由于前两趟的插入排序中记录的关键字是和同一子序列中的前一个记录的关键字进行比较,因此关键字较小的记录就不是一步一步地向前挪动,而是跳跃式地往前移,从而使得进行最后一趟排序时,整个序列已经做到基本有序,只要作记录的少量比较和移动即可。因此希尔排序的效率要比直接插入排序高。希尔排序的分析是复杂的,时间复杂度是所取增量的函数,这涉及一些数学上的难题。但是在大量实验的基础上推出当n在某个范围内时,时间复杂度可以达到O(n^1.3)。
public static void shellInsert(int[] arr, int d) {
for (int i = d; i < arr.length; i++) {
int j = i - d;
int temp = arr[i]; //记录要插入的数据
while (j >= 0 && arr[j] > temp) { //从后向前,找到比其小的数的位置
arr[j + d] = arr[j]; //向后挪动
j -= d;
}
if (j != i - d) //存在比其小的数
arr[j + d] = temp;
}
}
public static void shellSort(int[] arr) {
if (arr == null || arr.length == 0)
return;
int d = arr.length / 2;
while (d >= 1) {
shellInsert(arr, d);
d /= 2;
}
}
归并排序
归并排序是另一种不同的排序方法,因为归并排序使用了递归分治的思想,所以理解起来比较容易。其基本思想是,先递归划分子问题,然后合并结果。把待排序列看成由两个有序的子序列,然后合并两个子序列,然后把子序列看成由两个有序序列。。。。。倒着来看,其实就是先两两合并,然后四四合并最终形成有序序列。空间复杂度为O(n),时间复杂度为O(nlogn)。
public static void mergeSort(int[] arr) {
mSort(arr, 0, arr.length - 1);
}
/**
* 递归分治
*
* @param arr 待排数组
* @param left 左指针
* @param right 右指针
*/
public static void mSort(int[] arr, int left, int right) {
if (left >= right)
return;
int mid = (left + right) / 2;
mSort(arr, left, mid); //递归排序左边
mSort(arr, mid + 1, right); //递归排序右边
merge(arr, left, mid, right); //合并
}
public static void merge(int[] arr, int left, int mid, int right) {
//[left, mid] [mid+1, right]
int[] temp = new int[right - left + 1]; //中间数组
int i = left;
int j = mid + 1;
int k = 0;
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
while (i <= mid) {
temp[k++] = arr[i++];
}
while (j <= right) {
temp[k++] = arr[j++];
}
for (int p = 0; p < temp.length; p++) {
arr[left + p] = temp[p];
}
}
计数排序
如果在面试中有面试官要求你写一个O(n)时间复杂度的排序算法,你千万不要立刻说:这不可能!虽然前面基于比较的排序的下限是O(nlogn)。但是确实也有线性时间复杂度的排序,只不过有前提条件,就是待排序的数要满足一定的范围的整数,而且计数排序需要比较多的辅助空间。其基本思想是,用待排序的数作为计数数组的下标,统计每个数字的个数。然后依次输出即可得到有序序列。
public static void countSort(int[] arr) {
if (arr == null || arr.length == 0)
return;
int max = max(arr);
int[] count = new int[max + 1];
Arrays.fill(count, 0);
for (int i = 0; i < arr.length; i++) {
count[arr[i]]++;
}
int k = 0;
for (int i = 0; i <= max; i++) {
for (int j = 0; j < count[i]; j++) {
arr[k++] = i;
}
}
}
public static int max(int[] arr) {
int max = Integer.MIN_VALUE;
for (int ele : arr) {
if (ele > max)
max = ele;
}
return max;
}
桶排序
public static void bucketSort(int[] arr) {
if (arr == null && arr.length == 0)
return;
int bucketNums = 10; //这里默认为10,规定待排数[0,100)
List<List<Integer>> buckets = new ArrayList<List<Integer>>(); //桶的索引
for (int i = 0; i < 10; i++) {
buckets.add(new LinkedList<Integer>()); //用链表比较合适
} //划分桶
for (int i = 0; i < arr.length; i++) {
buckets.get(f(arr[i])).add(arr[i]);
} //对每个桶进行排序
for (int i = 0; i < buckets.size(); i++) {
if (!buckets.get(i).isEmpty()) {
Collections.sort(buckets.get(i)); //对每个桶进行快排
}
} //还原排好序的数组
int k = 0;
for (List<Integer> bucket : buckets) {
for (int ele : bucket) {
arr[k++] = ele;
}
}
}
/**
* 映射函数
*
* @param x
* @return
*/
public static int f(int x) {
return x / 10;
}
基数排序
public static void radixSort(int[] arr) {
if (arr == null && arr.length == 0)
return;
int maxBit = getMaxBit(arr);
for (int i = 1; i <= maxBit; i++) {
List<List<Integer>> buf = distribute(arr, i); //分配
collecte(arr, buf); //收集
}
}
/**
* 分配
*
* @param arr 待分配数组
* @param iBit 要分配第几位
* @return
*/
public static List<List<Integer>> distribute(int[] arr, int iBit) {
List<List<Integer>> buf = new ArrayList<List<Integer>>();
for (int j = 0; j < 10; j++) {
buf.add(new LinkedList<Integer>());
}
for (int i = 0; i < arr.length; i++) {
buf.get(getNBit(arr[i], iBit)).add(arr[i]);
}
return buf;
}
/**
* 收集
*
* @param arr 把分配的数据收集到arr中
* @param buf
*/
public static void collecte(int[] arr, List<List<Integer>> buf) {
int k = 0;
for (List<Integer> bucket : buf) {
for (int ele : bucket) {
arr[k++] = ele;
}
}
}
/**
* 获取最大位数
*
* @param
* @return
*/
public static int getMaxBit(int[] arr) {
int max = Integer.MIN_VALUE;
for (int ele : arr) {
int len = (ele + "").length();
if (len > max)
max = len;
}
return max;
}
/*** 获取x的第n位,如果没有则为0.
* @param x
* @param n
* @return
*/
public static int getNBit(int x, int n) {
String sx = x + "";
if (sx.length() < n)
return 0;
else
return sx.charAt(sx.length() - n) - '0';
}