实现逻辑:相邻两个元素两两相比较,按照排序规则(升序大的数放后面,降序小的数放后面)进行排序
时间复杂度O(n2)
空间复杂度O(1)
稳定排序
优化思路:通过加入布尔变量,判断内层循环是否发生交换来优化冒泡排序,最好情况为数组有序,复杂度降低为O(n),最坏情况为完全倒序,复杂度为O(n2)。
public static void sort(int[] arr) {
boolean change = false;
int temp;
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { //后者大于前者则交换位置
change = true;
temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
if (!change) { //判断是否发生交换
break;
}
}
}
实现逻辑:将数组分为已排序和未排序,从未排序列表中选取最小值和未排序的第一个元素交换位置。
时间复杂度为O(n2)
空间复杂度为O(1)
稳定排序
相对于冒泡排序效率更高,因为内循环少了许多赋值语句,一次内循环最多只需进行一次位置交换即可。
public static void sort(int[] arr) {
int length = arr.length;
if (length == 1) {
return;
}
for (int i = 0; i < length; i++) {
int min = arr[i];
int index = i;
for (int j = i + 1; j < length; j++) {
if (arr[j] < min) {
min = arr[j];
index = j;
}
}
if (index != i) {
int tmp = arr[index];
arr[index] = arr[i];
arr[i] = tmp;
}
}
}
实现逻辑:在要排序的一组数中,假定前n-1个数已经排好序,现在将第n个数插到前面的有序数列中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。
时间复杂度为O(n2)
空间复杂度为O(1)
稳定排序
优化思路:插入排序分为交换法和复制法,采用复制法可以减少交换法中大量的赋值语句从而能够更快地完成排序。
public static void sort(int[] arr) {
int length = arr.length;
for (int i = 1; i < length; i++) {
int curNum = arr[i];
int targetIndex = i; //该索引指向最终要插入的位置
for (int j = i - 1; j >= 0; j--) {
if (arr[j]>curNum){
arr[j+1] = arr[j]; //arr[j]后移一位
targetIndex = j;
}else{
break; //否则退出内循环
}
}
arr[targetIndex] = curNum;
}
}
实现逻辑:在要排序的一组数中,根据某一增量分为若干子序列,并对子序列分别进行插入排序。然后逐渐将增量减小,并重复上述过程。直至增量为1,此时数据序列基本有序,最后进行插入排序。
时间复杂度O(n^(1.5))
空间复杂度
非稳定排序
public static void sort(int[] arr) {
int temp = 0;
int length = arr.length;
int incre = length;
while (true) {
incre = incre / 2;
for (int k = 0; k < incre; k++) { //根据增量分为若干子序列
for (int i = k + incre; i < length; i += incre) {
for (int j = i; j > k; j -= incre) {
if (arr[j] < arr[j - incre]) {
temp = arr[j - incre];
arr[j - incre] = arr[j];
arr[j] = temp;
} else {
break;
}
}
}
}
if (incre == 1) {
break;
}
}
}
实现逻辑:利用分治法的思想,将数组从中间分成前后两个部分,然后分别排序,最后将排序后的结果合并。
时间复杂度为O(nlogn)
空间复杂度O(n)
稳定排序
代码实现上用到了递归算法,核心的方法为分解与合并。
public static int[] sort(int[] arr) {
return mergeSortChild(arr, 0, arr.length - 1);
}
private static int[] mergeSortChild(int[] arr, int p, int r) {
if (p >= r) {
return new int[]{arr[r]};
}
int q = (p + r) / 2;
int[] arr1 = mergeSortChild(arr, p, q);
int[] arr2 = mergeSortChild(arr, q + 1, r);
return merge(arr1, arr2);
}
private static int[] merge(int[] arr1, int[] arr2) {
int n = arr1.length + arr2.length;
int[] res = new int[n];
int i = 0;
int j = 0;
for (int k = 0; k < n; k++) {
if (i == arr1.length) {
res[k] = arr2[j++];
} else if (j == arr2.length) {
res[k] = arr1[i++];
} else {
if (arr1[i] <= arr2[j]) {
res[k] = arr1[i++];
} else {
res[k] = arr2[j++];
}
}
}
return res;
}
实现逻辑:如果要排序的数组中下标从p到r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为pivot(分区点)。遍历 p 到 r 之间的数据,将小于 pivot 的放在左边,将大于 pivot 的放到右边,将pivot放到中间。经过这一步骤,数组 p 到 r 之间数据被分成了三个部分,前面 p 到 q-1 之间的都是小于pivot的,中间是pivot,后面的 q+1到 r 之间的是大于pivot的。
时间复杂度为O(nlogn)~O(n2)
如果每次分区操作,都能正好把数组分成大小接近的两个小区,那快排的时间复杂度和归并排序相同,也是O(nlogn)。如果在极端情况下,每次分区,两个小区的大小差别都很悬殊,那么它的时间复杂度会退化为O(n2),具体怎么算,可以用递归树的相关知识来解答。快速排序算法虽然在最坏情况下时间复杂度是O(n2)。但最好情况,平均情况时间复杂度都是O(nlogn)。而且,快速排序算法时间复杂度退化为O(n^2) 的概率非常小,我们可以通过合理地选择pivot来避免这种情况。
空间复杂度O(1)
不稳定排序
public static void sort(int[] arr){
quickSort(arr, 0, arr.length - 1);
}
private static void quickSort(int[] arr, int p, int r) {
if (p >= r){
return;
}
int q = partition(arr, p , r);
quickSort(arr, p, q-1);
quickSort(arr, q+1, r);
}
private static int partition(int[] arr, int p, int r) {
int cursor = r;
boolean findBig = false;
for (int i = 0; i < r; i++) {
if (arr[i] > arr[r] && !findBig){
cursor = i;
findBig = true;
}
if (arr[i] < arr[r] && findBig){
int tmp = arr[i];
arr[i] = arr[cursor];
arr[cursor] = tmp;
cursor++;
}
}
if (cursor != r){
int tmp = arr[r];
arr[r] = arr[cursor];
arr[cursor] = tmp;
}
return cursor;
}
实现逻辑:堆满足完全二叉树的性值,分为"最大堆"和"最小堆"。最大堆通常被用来进行"升序"排序,而最小堆通常被用来进行"降序"排序。通过不断地建堆,首尾交换,从而完成排序。
/**
* 对数组进行堆排序
* @param arr 待排序列
*/
private static void heapSort(int[] arr) {
//创建堆
for (int i = (arr.length - 1) / 2; i >= 0; i--) {
//从第一个非叶子结点从下至上,从右至左调整结构
adjustHeap(arr, i, arr.length);
}
//调整堆结构+交换堆顶元素与末尾元素
for (int i = arr.length - 1; i > 0; i--) {
//将堆顶元素与末尾元素进行交换
int temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
//重新对堆进行调整
adjustHeap(arr, 0, i);
}
}
/**
* 调整堆
* @param arr 待排序列
* @param parent 父节点
* @param length 待排序列尾元素索引
*/
private static void adjustHeap(int[] arr, int parent, int length) {
//将temp作为父节点
int temp = arr[parent];
//左孩子
int lChild = 2 * parent + 1;
while (lChild < length) {
//右孩子
int rChild = lChild + 1;
// 如果有右孩子结点,并且右孩子结点的值大于左孩子结点,则选取右孩子结点
if (rChild < length && arr[lChild] < arr[rChild]) {
lChild++;
}
// 如果父结点的值已经大于孩子结点的值,则直接结束
if (temp >= arr[lChild]) {
break;
}
// 把孩子结点的值赋给父结点
arr[parent] = arr[lChild];
//选取孩子结点的左孩子结点,继续向下筛选
parent = lChild;
lChild = 2 * lChild + 1;
}
arr[parent] = temp;
}
实现逻辑:基数排序(radix sort)属于"分配式排序"(distribution sort),又称"桶子法"(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些"桶"中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog®m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
/**
* 递归,找出数组最大的值
*
* @param arrays 数组
* @param L 左边界,第一个数
* @param R 右边界,数组的长度
* @return
*/
public static int findMax(int[] arrays, int L, int R) {
//如果该数组只有一个数,那么最大的就是该数组第一个值了
if (L == R) {
return arrays[L];
} else {
int a = arrays[L];
int b = findMax(arrays, L + 1, R);//找出整体的最大值
if (a > b) {
return a;
} else {
return b;
}
}
}
public static void sort(int[] arrays) {
int max = findMax(arrays, 0, arrays.length - 1);
//需要遍历的次数由数组最大值的位数来决定
for (int i = 1; max / i > 0; i = i * 10) {
int[][] buckets = new int[arrays.length][10];
//获取每一位数字(个、十、百、千位...分配到桶子里)
for (int j = 0; j < arrays.length; j++) {
int num = (arrays[j] / i) % 10;
//将其放入桶子里
buckets[j][num] = arrays[j];
}
//回收桶子里的元素
int k = 0;
//有10个桶子
for (int j = 0; j < 10; j++) {
//对每个桶子里的元素进行回收
for (int l = 0; l < arrays.length; l++) {
//如果桶子里面有元素就回收(数据初始化会为0)
if (buckets[l][j] != 0) {
arrays[k++] = buckets[l][j];
}
}
}
}
}