冒泡排序
冒泡排序会两两比较每个元素的大小,并将其排序。每一趟排序都会确定一个元素的位置,把最大的放在最后面,并且在排序的过程中会将小的元素集中到前面,大的元素集中到后面,冒泡排序是稳定的
时间复杂度O(n^2),空间复杂度O(1)
public static int[] bubbleSort(int[] nums){
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < nums.length-i-1; j++) {
if(nums[j]>nums[j+1]){
nums[j]=nums[j]+nums[j+1];
nums[j+1]=nums[j]-nums[j+1];
nums[j]=nums[j]-nums[j+1];
}
}
}
return nums;
}
选择排序
选择排序会在每趟选取一个最小的元素,并将其和开头元素进行交换。每一趟排序都会确定一个元素的位置,把最小的放在前面,但由于有两个相同元素时会选择位置在后面的那个先放到前面,因此选择排序是不稳定的
时间复杂度O(n^2),空间复杂度O(1)
public static int[] selectSort(int[] nums) {
for (int i = 0; i < nums.length - 1; i++) {
int minIndex=i;
int min=nums[i];
for (int j = i + 1; j < nums.length; j++) {
if (min > nums[j]) {
minIndex=j;
min=nums[j];
}
}
if(minIndex==i)continue;
nums[i] = nums[i] + nums[minIndex];
nums[minIndex] = nums[i] - nums[minIndex];
nums[i] = nums[i] - nums[minIndex];
}
return nums;
}
插入排序
插入排序将列表分为两部分,一个是前面的有序部分,一个是后面的无序部分。从后面的无序部分选取第一个元素插入到前面有序部分中,指导无序部分长度变为0。插入排序稳定
时间复杂度O(n^2),空间复杂度O(1)。虽然时间复杂度一样,但是一般来说插入排序的效率会比冒泡和选择快一点。
public static int[] insertSort(int[] nums){
for (int i = 1; i < nums.length; i++) {// 插入排序
int j=i;
int temp=nums[i];
while(j>0&&temp<nums[j-1]){
nums[j]=nums[j-1];
j-=1;
}
nums[j]=temp;
}
return nums;
}
希尔排序
希尔排序是插入排序的优化版本,他先按相距一定间隔在原数组中选择元素进行排序,之后逐步减少间隔的距离直到距离为一。
希尔排序是稳定的
时间复杂度大概O(n^1.3),空间复杂度O(1)。
public static int[] shellSort(int[] nums) {
for (int inc = nums.length / 2; inc > 0; inc = inc / 2) {// 间隔距离
for (int i = inc; i < nums.length; i++) {// 插入排序
int j=i;
int temp=nums[i];
while(j>=inc&&temp<nums[j-inc]){
nums[j]=nums[j-inc];
j-=inc;
}
nums[j]=temp;
}
}
return nums;
}
快速排序
快速排序选取一个值value作为参照(一般是数组的第一个值),把比value大的放到数组的后半部分,比value小的放到数组的前半部分。然后递归的在前半部分和后半部分继续重复上述的操作。
时间复杂度O(nlogn),空间复杂度O(logn)
快速排序是不稳定的
public static int[] fastSort(int[] nums, int bg, int end) {
if (bg >= end)
return nums;
int last = end;
int first = bg;
int value = nums[bg];
while (first < last) {
if (nums[last] > value) {
last--;
} else {// 调头
nums[first] = nums[last];
while (first < last) {
if (nums[first] < value) {
first++;
} else {
nums[last--] = nums[first];
break;
}
}
}
}
nums[first] = value;
fastSort(nums, bg, last);
fastSort(nums, last + 1, end);
return nums;
}
归并排序
归并排序首先将数组递归的向下对半划分,划分到不能划分(每个子序列只有一个元素)后再将子序列向上合并成有序序列,直到合并为最后一个有需序列。
时间复杂度O(nlogn),空间复杂度O(n)。(需要一个辅助数组来合并两个序列)。
归并排序是稳定的
public static int[] mergeSort(int[] nums, int bg, int end) {
int mid = (bg + end) / 2;
if (bg >= end)
return nums;
mergeSort(nums, bg, mid);
mergeSort(nums, mid + 1, end);
return merge(nums, bg, end);
}
public static int[] merge(int[] nums, int bg, int end) {
int temp[] = new int[end - bg + 1];
int mid = (bg + end) / 2;
int index = 0;
int i = bg, j = mid + 1;
while (i <= mid && j <= end) {
if (nums[i] > nums[j]) {
temp[index++] = nums[j++];
} else {
temp[index++] = nums[i++];
}
}
while (i <= mid) {
temp[index++] = nums[i++];
}
while (j <= end) {
temp[index++] = nums[j++];
}
for (int a = 0; a < temp.length; a++) {
nums[bg++] = temp[a];
}
return nums;
}
堆排序
什么是堆
堆是一个完全二叉树,且符合以下条件(小根堆就反一下)
- 节点大于等于根节点
- 左右子树都是符合上述要求
我们可以很完美的使用一维数组来表示一个堆。
由于完全二叉树的特殊结构,我们可以获得几条很有用的规律,设一颗完全二叉树的节点数量为n。位置表示在一维坐标中的索引(i从0开始)
- 第i个节点的左孩子位置为2i+1
- 第i个节点的右孩子位置为2i+2
- 第i个节点的父节点位置为 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor ⌊i/2⌋
综上我们可以用一个一维数组来表示一个堆,并且在数组上模拟对堆的操作
例如,交换第3个节点和他的右孩子的值,就只需要交换数组下标2和6的值即可。
构造堆
向上调整
下面以小根堆为例
在堆的最后添加一个元素,然后比较这个元素和他父节点的值大小,小于父节点就交换两个节点的值,然后继续与上一级的父节点比较。
向下调整
找到最后一个非叶子节点,
⌊
s
i
z
e
/
2
⌋
\lfloor size/2 \rfloor
⌊size/2⌋的位置。对这个节点进行调整,让他变成堆。然后找他上一个子树
⌊
s
i
z
e
/
2
⌋
\lfloor size/2 \rfloor
⌊size/2⌋-1,重复这个操作直到根节点。
/**
* 调整堆
* @param nums
* @param size 堆最后一个元素坐标
* @param last 当前调整位置
* @return
*/
public static int[] heapAdj(int[] nums, int size, int last) {
int maxIndex = last;
while (last <= (size + 1) / 2 - 1) {
if (last * 2 + 1 > size) {// 是叶子节点
return nums;
} else if (last * 2 + 2 <= size) {// 有两个孩子
if (nums[last * 2 + 1] >= nums[last * 2 + 2]) {
maxIndex = last * 2 + 1;
} else if (nums[last * 2 + 1] < nums[last * 2 + 2]) {
maxIndex = last * 2 + 2;
}
if (nums[last] < nums[maxIndex]) {
swap(nums, last, maxIndex);
heapAdj(nums, size, maxIndex);
} else
break;
} else if (last * 2 + 1 <= size) {// 只有左孩子
if (nums[last] < nums[last * 2 + 1]) {
maxIndex = last * 2 + 1;
swap(nums, last, maxIndex);
heapAdj(nums, size, maxIndex);
} else
break;
}
}
return nums;
}
堆排序
首先将原始数据修改成堆,然后交换第一个元素和最后一个元素。然后再把这个数据调整成堆,重复操作即可。
时间复杂度O(nlogn),空间复杂度O(1)
堆排序不稳定
public static int[] heapSort(int[] nums) {
downAdj(nums);// 调整为初始堆
for (int i = 0; i < nums.length; i++) {// 自顶向下调整为堆
swap(nums, 0, nums.length - i - 1);
heapAdj(nums, nums.length - i - 2, 0);
}
return nums;
}
/**
* 创建大根堆
*
* @param nums
* @return
*/
public static int[] downAdj(int[] nums) {
int parent = nums.length / 2 - 1;
for (int i = parent; i >= 0; i--) {
heapAdj(nums, nums.length - 1, i);
}
return nums;
}
总结
排序算法 | 时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|
冒泡排序 | n 2 n^2 n2 | 1 | 稳定 |
插入排序 | n 2 n^2 n2 | 1 | 稳定 |
选择排序 | n 2 n^2 n2 | 1 | 不稳定 |
希尔排序 | n 1.3 n^{1.3} n1.3 | 1 | 不稳定 |
快速排序 | n l o g n nlogn nlogn | 1 | 不稳定 |
归并排序 | n l o g n nlogn nlogn | n n n | 稳定 |
堆排序 | n l o g n nlogn nlogn | 1 | 不稳定 |