冒泡排序(Bubble Sort)
public static void bubbleSort(int[] nums) {
int size = nums.length;
// 每轮针对前面(size-i)个数进行排序
for (int i = 0; i < size - 1; i++) {
System.out.println("第" + (i + 1) + "轮交换开始");
// 每一轮排序,从第 0 个元素,到 size-1-i 个元素
for (int j = 0; j < size - 1 - i; j++) {
// 对比相邻的两个元素
if (nums[j] > nums[j + 1]) {
// 元素交换。使得大的元素在后面
int temp = nums[j + 1];
nums[j + 1] = nums[j];
nums[j] = temp;
}
printf(nums);
}
}
}
冒泡排序(Bubble Sort)是基于交换的排序,每次遍历需要排序的元素,依次比较相邻的两个元素的大小,如果前一个元素大于后一个元素则两者交换,保证最后一个数字一定是最大的(假设按照从小到大排序),即最后一个元素已经排好序,下一轮只需要保证前面 n-1
个元素的顺序即可。
由于冒泡排序只会交换相邻的元素,它不会出现两个相等的元素,后面的元素被交换到前面去的情况(相等的时候,我们可以控制两者不交换),所以冒泡排序是稳定的。
上面计算时间复杂度是 O(n2),时间复杂度取系数最高的项即可,当然这是最坏情况下的时间复杂度。
选择排序(SelectionSort)
public class SelectionSort {
public static void main(String[] args) {
int[]nums = new int[]{98,90,34,56,21};
printf(nums);
selectionSort(new int[]{98,90,34,56,21});
}
private static int[] selectionSort(int[] nums) {
int times = 0;
int minIndex ,temp;
for (int i = 0; i < nums.length - 1; i++) {
System.out.println("第" + (i+1) +"轮选择开始:");
minIndex = i;
for(int j = i+1; j < nums.length ; j++) {
times++;
if(nums[minIndex] > nums[j]) {
minIndex = j;
}
}
System.out.println("交换"+nums[i] + "和"+nums[minIndex]);
temp = nums[i];
nums[i] = nums[minIndex];
nums[minIndex] = temp;
}
System.out.println("比较次数" +times);
return nums;
}
private static void printf(int[] nums) {
for (int i = 0; i < nums.length; i++) {
System.out.print(nums[i]+" ");
}
}
}
前面说的冒泡排序是每一轮比较确定最后一个元素,中间过程不断地交换。而选择排序就是每次选择剩下的元素中最小的那个元素,与当前索引位置的元素交换,直到所有的索引位置都选择完成。
由于选择排序只会选择最小的元素进行交换,如果我们可以保证我们每次选择到的最小元素是第一次出现的(就算后面出现大小相等的元素我们也不会选择后面的),那么就可以保证它的稳定性,所以选择排序是可以做到稳定的。O(n2)
选择排序和冒泡排序的区别是什么呢?
冒泡排序在比较的时候会不断的交换,直到最大/最小的冒泡到最后的位置,而选择排序则是在剩下的元素里面选择出最小或者最大的,中间一般不会发生交换,只会维护好最小/最大值的索引,与当前需要排序的位置元素交换。
插入排序(InsertSort
插入排序是依次选择一个元素,插入到前面已经排好序的数组中间,确保它处于正确的位置,当然,这是需要已经排好的顺序数组不断移动。步骤描述如下:
- 从第一个元素开始,可以认为第一个元素已经排好顺序。
- 取出后面一个元素
n
,在前面已经排好顺序的数组里从尾部往头部遍历,假设正在遍历的元素为nums[i]
,如果num[i]
>n
,那么将nums[i]
移动到后面一个位置,直到找到已经排序的元素小于或者等于新元素的位置,将n
放到新腾空出来的位置上。如果没有找到,那么nums[i]
就是最小的元素,放在第一个位置。 - 重复上面的步骤 2,直到所有元素都插入到正确的位置。
public static void insertionSort(int[] nums) { if (nums == null) { return; } int size = nums.length; int index, temp; for (int i = 1; i < size; i++) { // 当前选择插入的元素前面一个索引值 index = i - 1; // 当前需要插入的元素 temp = nums[i]; while (index >= 0 && nums[index] > temp) { nums[index + 1] = nums[index]; index--; } // 插入空出来的位置 nums[index + 1] = temp; System.out.print("第" + (i) + "轮插入结果:"); printf(nums); } }
由于插入排序只会选择元素插入到适合的位置,只要我们按照原来的顺序遍历,即使相等的两个元素最后排完顺序之后,也会保持原有的相对顺序,所以插入排序是稳定的。O(n2)
计数排序(CountSort)
由于只做计数,不会大部分交换,根据统计数组来回复排序数组的时候是可以保持元素的相对位置的,所以是稳定排序。
- 遍历数组,找出最大值和最小值。
- 根据最大值和最小值,初始化对应的统计元素数量的数组。
- 遍历元素,统计元素个数到新的数组。
- 遍历统计的数组,按照顺序输出排序的数组元素。
public class CountSort { public static void countSort(int[] nums) { int max = nums[0]; int min = nums[0]; for (int i = 1; i < nums.length; i++) { if (nums[i] > max) { max = nums[i]; } if (nums[i] < min) { min = nums[i]; } } System.out.println("min:" + min + ",max:" + max); int count = max - min; int[] countNums = new int[count + 1]; for (int i = 0; i < nums.length; i++) { countNums[nums[i] - min]++; } System.out.print("countNums: "); printf(countNums); int sum = 0; // 后面的元素等于前面元素加上自身 for (int i = 0; i < count + 1; i++) { sum += countNums[i]; countNums[i] = sum; } System.out.print("countNums: "); printf(countNums); int[] newNums = new int[nums.length]; for (int i = nums.length - 1; i >= 0; i--) { /** * nums[i] - min 表示原数组 nums 里面第i位置对应的数在统计数组里面的位置索引 */ newNums[countNums[nums[i] - min] - 1] = nums[i]; countNums[nums[i] - min]--; } printf(newNums); } }
计数排序,在一定范围数值的数组,假设元素在
0~k
之间,一共 n 个数,那么只需要遍历 n 个数,就可以统计,统计的时候,只需要遍历 k 个数,就可以将排序的元素移动到数组中。时间复杂度为 O(n+k),申请了一个统计数组和一个新数组,空间复杂度为 O(n+k)。计数排序有严重的缺点:
- 如果数列的数组最大值与最小值相差太大,会浪费较大空间。
- 数组元素不是整数不适合计数排序。
基数排序(RadixSort)
基数排序比较特殊,特殊在它只能用在整数(自然数)排序,而且不是基于比较的,其原理是将整数按照位分成不同的数字,按照每个数各位值逐步排序。何为高位,比如 81,1 就是低位, 8 就是高位。 分为高位优先和低位优先,先比较高位就是高位优先,先比较低位就是低位优先。下面我们讲高位优先。
public class RadixSort {
private static void radixSort(int[] nums) {
int max = nums[0];
// 指数,从个位到十位到百位...
int exp;
// 遍历得到最大值
for (int num : nums) {
if (num > max) {
max = num;
}
}
// 从个位开始,对数组每一位进行排序
for (exp = 1; max / exp > 0; exp = exp * 10) {
// 临时数组
int[] tempNums = new int[nums.length];
// 数值 0-9,桶的个数固定为 10
int[] buckets = new int[10];
// buckets 中存储的其实是数据出现的次数
for (int value : nums) {
buckets[(value / exp) % 10]++;
}
// 每一个值等于前面的元素次数加上自身(类似计数排序)
for (int i = 1; i < 10; i++) {
buckets[i] += buckets[i - 1];
}
// 从后往前遍历,将元素写会临时数组
for (int i = nums.length - 1; i >= 0; i--) {
tempNums[buckets[(nums[i] / exp) % 10] - 1] = nums[i];
buckets[(nums[i] / exp) % 10]--;
}
// 将有序元素 tempNums 赋给 nums
System.arraycopy(tempNums, 0, nums, 0, nums.length);
printf(nums);
}
}
}
一般只使用于整数排序,不适合小数或者文字排序.时间O(n)
希尔排序(ShellSort)❤
希尔排序(Shell's Sort)又称“缩小增量排序”(Diminishing Increment Sort),是插入排序的一种更高效的改进版本,同时该算法是首次冲破 O(𝑛2n2) 的算法之一。
希尔排序也是面试官比较喜欢问的一个话题,它的特点是排序的间隔 gap
不断缩小。
- 选择一个增量
gap
,一般开始是数组的一半,将数组元素按照间隔为gap
分为若干个小组。 - 对每一个小组进行插入排序。
- 将
gap
缩小为一半,重新分组,重复步骤 2(直到gap
为 1 的时候基本有序,稍微调整一下即可)。
public static void shellSort(int[] nums) {
int times = 1;
for (int gap = nums.length / 2; gap > 0; gap /= 2) {
System.out.print(
"第" + (times++) + "轮希尔排序, gap= " + gap + " ,结果:"
);
for (int i = gap; i < nums.length; i++) {
int j = i;
int temp = nums[j];
while (j - gap >= 0 && temp < nums[j - gap]) {
// 移动法
nums[j] = nums[j - gap];
j -= gap;
}
nums[j] = temp;
}
printf(nums);
}
}
每次取数组的一半,希尔增量下最坏的情况时间复杂度是 O(𝑛2n2),最好的时间复杂度是 O(𝑛n) (也就是数组已经有序),平均时间复杂度是 O(𝑛3/2n3/2)
由于希尔排序,在分组的时候,会将后面的元素间隔性的调动到前面,所以会打乱原本两个相等的数之间的相对顺序,所以希尔排序是不稳定的排序算法。
快速排序(QuickSort)❤❤
快速排序比较有趣,选择数组的一个数作为基准数,一趟排序,将数组分割成为两部分,一部分均小于/等于基准数,另外一部分大于/等于基准数。然后分别对基准数的左右两部分继续排序,直到数组有序。这体现了分而治之的思想,其中还应用到挖坑填数的策略。
nums 0 nums.length-1
System.out.println("["+left+" , "+right+"]");
if(left < right) {
// 区间左边界是i,右边界是j,基准值是 standardNum
int i = left, j = right,standardNum = nums[left];
while (i < j) {
//从右向左找第一个小于 standard
while (i < j && nums[j] >= standardNum) {
j--;
}
System.out.println("standardNum = "+standardNum+", 第一个小于等于standardNum = "+nums[i]);
if (i < j) {
//nums[i]已经被保存到standardNum 将nums【i】写到左边
nums[i] = nums[j];
i++;
}
// 从左向右找第一个大于等于standardNum的数
while (i < j && nums[i] <= standardNum) {
i++;
}
System.out.println(",第一个大于等于standardNum = "+nums[i]);
if (i < j) {
//将较大的数写到右边
nums[j] = nums[i];
j--;
}
}
// 将基准值写到中间
nums[i] = standardNum;
printf(nums);
quickSort(nums,left,i-1);
printf(nums);
quickSort(nums,i+1,right);
由于快速排序会将一个数大间隔的移动到一边,大的数放在右边,小的数放在左边,所以会破坏两个相等的元素的相对顺序,所以它是不稳定的排序算法。
桶排序(BucketSort)
桶排序,是指用多个桶存储元素,每个桶有一个存储范围,先将元素按照范围放到各个桶中,每个桶中是一个子数组,然后再对每个子数组进行排序,最后合并子数组,成为最终有序的数组。这其实和计数排序很相似,只不过计数排序每个桶只有一个元素,而且桶存储的值为该元素出现的次数。
public static void bucketSort(int[] nums) {
// 遍历数组获取最大最小值
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int i = 0; i < nums.length; i++) {
max = Math.max(max, nums[i]);
min = Math.min(min, nums[i]);
}
// 计算桶的数量
int bucketNum = (max - min) / nums.length + 1;
System.out.println(
"最小:" + min + ",最大:" + max + ",桶的数量:" + bucketNum
);
List<List<Integer>> buckets = new ArrayList<List<Integer>>(bucketNum);
for (int i = 0; i < bucketNum; i++) {
buckets.add(new ArrayList<Integer>());
}
// 将每个元素放入桶
for (int i = 0; i < nums.length; i++) {
int num = (nums[i] - min) / (nums.length);
buckets.get(num).add(nums[i]);
}
// 对每个桶内部进行排序
for (int i = 0; i < buckets.size(); i++) {
Collections.sort(buckets.get(i));
}
// 将桶的元素复制到数组中
int index = 0;
for (int i = 0; i < buckets.size(); i++) {
for (int j = 0; j < buckets.get(i).size(); j++) {
nums[index++] = buckets.get(i).get(j);
}
}
}
public static void printf(int[] nums) {
for (int num : nums) {
System.out.print(num + " ");
}
System.out.println("");
}
至于排序的稳定性,桶排序的稳定性取决于桶内部的排序,如果桶内使用快速排序则整体桶排序表现为不稳定排序。
堆排序(HeapSort)
堆排序,就是利用大顶堆或者小顶堆来设计的排序算法,是一种选择排序。堆是一种完全二叉树:
- 大顶堆:每个节点的数值都大于或者等于其左右孩子节点的数值。
- 小顶堆:每个节点的数值都小于或者等于其左右孩子节点的数值。
public static void heapSort(int[] nums) {
// 首先需要构建最大堆
for (int i = nums.length / 2 - 1; i >= 0; i--) {
// 从第一个非叶子结点调整结构,大的往上走
adjustHeap(nums, i, nums.length);
}
printf(nums);
System.out.println("-----------------------------");
// 交换元素和调整
for (int j = nums.length - 1; j > 0; j--) {
// 将堆顶元素与末尾元素交换
swap(nums, 0, j);
// 重新调整,大的元素往上交换
adjustHeap(nums, 0, j);
printf(nums);
System.out.println("-----------------------------");
}
}
/**
* 调整大顶堆
*/
public static void adjustHeap(int[] nums, int i, int length) {
// 取出当前元素
int temp = nums[i];
//从左节点开始
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
// 如果右节点更大,那么指向右节点
if (k + 1 < length && nums[k] < nums[k + 1]) {
k++;
}
// 子节点的值直接给父节点
if (nums[k] > temp) {
nums[i] = nums[k];
i = k;
} else {
break;
}
printf(nums);
}
// 最后将最上面的节点置,放到当前的节点
nums[i] = temp;
}
/**
* 交换元素
*/
public static void swap(int[] nums, int a, int b) {
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
public static void printf(int[] nums) {
for (int num : nums) {
System.out.print(num + " ");
}
System.out.println("");
}
}
由于在调整堆的时候,会修改相等元素的相对位置,所以堆排序是不稳定的排序算法。