1 选择排序
public static void selectionSort(int[] nums) {
if (nums == null || nums.length < 2) return;
for (int i = 0; i < nums.length; i++) {
int minIndex = i;
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] < nums[minIndex]) minIndex = j;
}
int tmp = nums[i];
nums[i] = nums[minIndex];
nums[minIndex] = tmp;
}
}
-
时间复杂度: O(N^2) 与数据状态无关
-
空间复杂度: O(1)
2 冒泡排序
public static void bubbleSort(int[] nums) {
for (int i = 0; i < nums.length; i++) {
for (int j = 1; j < nums.length - i; j++) {
if (nums[j] < nums[j - 1]) {
int tmp = nums[j];
nums[j] = nums[j - 1];
nums[j - 1] = nums[j];
}
}
}
}
-
时间复杂度: O(N^2) 与数据状态无关
-
空间复杂度: O(1)
3 插入排序
public static void insertSort(int[] nums) {
for (int i = 0; i < nums.length; i++) {
int j = i;
while (j > 0 && nums[j] < nums[j - 1]) {
int tmp = nums[j];
nums[j] = nums[j - 1];
nums[j - 1] = tmp;
j--;
}
}
}
-
时间复杂度:O(N^2) 与数据状态有关,最好的时间复杂度可以达到O(N)
-
空间复杂度:O(1)
4 归并排序
public static void mergeSort(int[] nums, int l ,int r) {
if (l == r) return;
int mid = l + (r - l) / 2;
mergeSort(nums, l, mid);
mergeSort(nums, mid + 1, r);
merge(nums, l, mid, r);
}
public static void merge(int[] nums, int l, int mid, int r) {
int[] help = new int[r - l + 1];
int idx1 = l; int idx2 = mid + 1;
int i = 0;
while (idx1 <= mid && idx2 <= r) {
if (nums[idx1] < nums[idx2]) {
help[i++] = nums[idx1++];
} else help[i++] = nums[idx2++];
}
while (idx1 <= mid) {
help[i++] = nums[idx1++];
}
while (idx2 <= r) {
help[i++] = nums[idx2++];
}
for (int j = 0; j < help.length; j++) {
nums[l + j] = help[j];
}
}
-
时间复杂度:O(NlogN)
T(N) = 2 * T(N / 2) + O(N)
根据master公式:a = 2 , b = 2, d = 1 ==> log(b, a) = d ==> O(NlogN)
-
空间复杂度:O(N)
扩展:
1、 小和问题, 求数组中某个数的左侧比他小的数之和为某个数的小和,求数组中所有数的小和之和的问题:
只需要对归并排序的代码加上一个全局变量记录即可。我以为这个题主要就是利用的归并排序的一个特点,就是说在归并的每一层左右两侧数组在原数组中的相对关系是一样的,比如[1, 3, 4, 2 ,5]最终归并处理的是[1, 3, 4]和[2, 5]:1, 3, 4在原数组中也都在2, 5的右侧
while (idx1 <= mid && idx2 <= r) {
if (nums[idx1] < nums[idx2]) {
res += nums[idx1] * (r - idx2 + 1);
help[i++] = nums[idx1++];
} else help[i++] = nums[idx2++];
}
2、逆序对问题:在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对
这个题和小和问题一样,都是利用的归并过程中数的相对位置不变的特点
5 快速排序
5.1 引子
快速排序的引子可以看LeetCode75 颜色排序,经典的荷兰国旗问题,快速排序的每一快排的思想就是这个;
5.2 初级版本
public static void quickSort(int[] nums, int l, int r) {
if (l >= r) return;
int i = l, j = r;
int num = nums[r];
int tmp = 0;
for (int k = l; k <= j; k++) {
if (nums[k] < num) {
tmp = nums[k];
nums[k] = nums[i];
nums[i] = tmp;
i++;
} else if (nums[k] > num) {
tmp = nums[k];
nums[k] = nums[j];
nums[j] = tmp;
j--;
k--;
}
}
quickSort(nums, l, i - 1);
quickSort(nums, j + 1, r);
}
时间复杂度:O(N ^ 2)
最极端的例子:[1, 2, 3, 4, 5, 6]
每一次执行快排函数都要遍历完n - 1个数,最后的复杂度就是O(N ^ 2)
5.3 优化版本
根据极端情况来看,我们每次快速排序的划分值(程序中的num)的选择是十分重要的
根据master公式,如果每次我们的划分值都能使得排序后处于数组中间位置就是能够降低时间复杂度到O(NlogN)
所以我们在选择num时,可以随机选择(l , r)之间的数,这样,最后我们的时间复杂度期望就是O(NlogN)
空间复杂度:O(logN) 递归的情况下,每一层释放的空间该层可以复用
public static void quickSort(int[] nums, int l, int r) {
if (l >= r) return;
int i = l, j = r;
int num = nums[l + (int)(Math.random()*(r - l + 1))];
int tmp = 0;
for (int k = l; k <= j; k++) {
if (nums[k] < num) {
tmp = nums[k];
nums[k] = nums[i];
nums[i] = tmp;
i++;
} else if (nums[k] > num) {
tmp = nums[k];
nums[k] = nums[j];
nums[j] = tmp;
j--;
k--;
}
}
quickSort(nums, l, i - 1);
quickSort(nums, j + 1, r);
}
6 堆排
大根堆实现
public static void heapSort(int[] nums) {
if (nums == null || nums.length < 2) return;
// for (int i = 0; i < nums.length; i++) {
// heapInsert(nums, i);
// }
int heapsize = nums.length;
for (int i = nums.length - 1; i >= 0; i--) {
heapify(nums, i, heapsize);
}
while (heapsize > 0) {
swap(nums, 0, --heapsize);
heapify(nums, 0, heapsize);
}
}
//某个数在idx位置,能否再往下移动
public static void heapify(int[] nums, int idx, int heapsize) {
int l = 2 * idx + 1;
while (l < heapsize) {
//寻找堆中最大元素的下标
//1 两个孩子中最大的一个
int largest = l + 1 < heapsize && nums[l + 1] > nums[l] ? l + 1 : l;
//2 和父节点比较
largest = nums[idx] > nums[largest] ? idx : largest;
if (largest == idx) break;
swap(nums, largest, idx);
idx = largest;
l = idx * 2 + 1;
}
}
//向堆中增加数据
public static void heapInsert(int[] nums, int idx) {
while (nums[idx] > nums[(idx - 1) / 2]) {
swap(nums, idx, (idx - 1) / 2);
idx = (idx - 1) / 2;
}
}
public static void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
- 时间复杂度:O(NlogN)
- 空间复杂度:O(N)
扩展
对一个近似有序的数组进行排序,所谓近似有序,就是说数组拍好顺序的话,每个元素移动的距离可以不超过k:
从头开始,k各一组构成小顶堆,然后弹出第一个元素就是整个数组的最小值,放在数组的第一个位置,这样以此类推即可
7 桶排序
public static void radixSort(int[] nums) {
if (nums == null || nums.length < 2) return;
radixSort(nums, 0, nums.length - 1, maxBit(nums));
}
//得到最大的数的位数
public static int maxBit(int[] nums) {
int max = nums[0];
for (int num : nums) {
max = Math.max(max, num);
}
int res = 0;
while (max > 0) {
res++;
max /= 10;
}
return res;
}
//得到某个数某一位的值
public static int getDigit(int num, int d) {
return ((int) (num / (Math.pow(10, d - 1)))) % 10;
}
//桶排序核心代码
public static void radixSort(int[] nums, int l, int r, int digit) {
final int radix = 10;
//辅助空间
int[] bucket = new int[r - l + 1];
int j = 0;
for (int d = 1; d <= digit; d++) { //最高位有多少位就需要进行多少次的桶排序
//count用来统计词频
//count[i]表示数组中的数第d位是i的个数
int[] count = new int[radix];
for (int i = l; i <= r; i++) {
j = getDigit(nums[i], d);
count[j]++;
}
/**将count改为前缀和,这样便于找到取出元素的顺序
* 按照从后往前的顺序出桶,则出桶元素应该放在辅助数组索引为count[j] - 1的位置
* 出桶后注意count[j]--
*/
for (int i = 1; i < radix; i++) {
count[i] += count[i - 1];
}
for (int i = r; i >= l; i--) {
j = getDigit(nums[i], d);
bucket[count[j] - 1] = nums[i];
count[j]--;
}
//将排序后的结果同步到原数组中去
for (int i = l; i <= r; i++) {
nums[i] = bucket[i - l];
}
}
}
8 排序算法的稳定性
-
概念
同样值的个体之间,如果不因为排序而改变相对次序,就说这个排序是有稳定性的,否则就没有
-
不具备稳定性的排序
选择排序,快速排序,堆排序
-
具备稳定性的排序
冒泡排序,插入排序,归并排序,桶排序