提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
6.1 排序算法介绍
排序也称排序算法(Sort Algorithm),是将一组数据,依指定的顺序进行排列的过程。
术语说明:
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
- 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
- 内排序:所有排序操作都在内存中完成;
- 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
- 时间复杂度:一个算法执行所耗费的时间。
- 空间复杂度:运行完一个程序所需内存的大小。
6.2 排序算法分类
内部排序:
指将需要处理的所有数据都加载到内部存储器(内存)中进行排序。
外部排序法:
数据量过大无法全部加载到内存中,需要借助外部存储(文件等)进行排序。
常见的排序算法分类:
6.3 排序算法的时间复杂度
比较和非比较的区别:
常见的快速排序、归并排序、堆排序、冒泡排序等属于比较排序。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。
在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logN次,所以时间复杂度平均O(nlogn)。
比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。
计数排序、基数排序、桶排序则属于非比较排序。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置。
非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。
非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。
6.4 排序算法的空间复杂度
6.5 冒泡排序
算法思想
冒泡排序(Bubble Sorting)的基本思想:是通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
算法优化
在排序的过程中各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志 flag 判断元素是否进行过交换,从而减少不必要的比较。
算法描述
- 比较相邻的元素,如果第一个比第二个大,就交换它们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 重复步骤1~3,直到排序完成。
算法实现
public class BubbleSortTest {
public static void main(String[] args) {
int[] array = CreateArrayUtils.createArray(8);
System.err.println(Arrays.toString(array));
bubbleSort(array);
}
private static void bubbleSort(int[] array) {
int length = array.length - 1;
boolean flag = false;
for (int i = 0; i < length; i++) {
for (int j = 0; j < length - 1 - i; j++) {
if (array[j] > array[j + 1]) {
flag = true;
// 交换相邻的值
array[j] = array[j] + array[j + 1];
array[j + 1] = array[j] - array[j + 1];
array[j] = array[j] - array[j + 1];
}
}
// 优化
if (!flag) {
break;
} else {
// 重置标志位
flag = false;
}
}
System.err.println(Arrays.toString(array));
}
}
6.6 选择排序
选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。
算法思想
选择排序(select sorting)的基本思想是:第一次从 arr[0]~arr[n-1] 中选取最小值与 arr[0] 交换,第二次从 arr[1]~arr[n-1] 中选取最小值与 arr[1]交换,第三次从 arr[2]~arr[n-1] 中选取最小值与 arr[2] 交换…第 i 次从 arr[i-1]~arr[n-1] 中选取最小值与 arr[i-1]交换…第 n-1 次从 arr[n-2]~arr[n-1] 中选取最小值与 arr[n-2]交换,总共通过 n-1 次得到一个按排序码从小到大排列的有序序列。
算法实现:
public class SelectSortTest {
public static void main(String[] args) {
int[] array = CreateArrayUtils.createArray(10);
System.err.println(Arrays.toString(array));
selectSort(array);
}
private static void selectSort(int[] array) {
int length = array.length;
for (int i = 0; i < length - 1; i++) {
int minIndex = i;
int min = array[i];
for (int j = i + 1; j < length; j++) {
if (min > array[j]) {
minIndex = j;
min = array[j];
}
}
// 交换值
if (minIndex != i) {
array[minIndex] = array[i];
array[i] = min;
}
}
System.err.println(Arrays.toString(array));
}
}
6.7 插入排序
插入排序属于内部排序法,对于欲排序的元素以插入的方式寻找该元素的适当位置,以达到排序的目的。
算法思想
插入排序(Insertion Sorting)的基本思想:把 n 个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有 n-1 个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
算法描述
- 从第一个元素开始,该元素可以认为已经被排序。
- 取出下一个元素,在已经排序的元素序列中从后向前扫描。
- 如果该元素(已排序)大于新元素,将该元素移到下一位置。
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置。
- 重复步骤2~5。
算法实现
public class InsertSortTest {
public static void main(String[] args) {
int[] array = CreateArrayUtils.createArray(8);
System.err.println(Arrays.toString(array));
insetSort(array);
}
private static void insetSort(int[] nums) {
for (int i = 1; i < nums.length; i++) {
int val = nums[i];
int j = i - 1;
for (; j >= 0; j--) {
if (nums[j] > val) {
nums[j + 1] = nums[j];
} else {
break;
}
}
nums[j + 1] = val;
}
System.err.println(Arrays.toString(nums));
}
}
6.8 希尔排序
希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
算法思想
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
算法描述
希尔排序的基本步骤:选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1。
- 按增量序列个数k,对序列进行k 趟排序
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序,仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度
算法实现
希尔排序时,,对有序序列在插入时可以采用交换法或移动法。
public class ShellSortTest {
public static void main(String[] args) {
int[] array = CreateArrayUtils.createArray(10);
System.err.println(Arrays.toString(array));
shellSort2(array);
}
private static void shellSort1(int[] array) {
int length = array.length;
// 按下标的一定增量分组,增量 gap,并逐步的缩小增量
for (int gap = length / 2; gap > 0; gap /= 2) {
// 遍历所有的分组
for (int i = gap; i < length; i++) {
// 遍历各组中所有的元素(共 gap 组,每组有 length/gap 个元素),步长 gap
for (int j = i - gap; j >= 0; j -= gap) {
// 交换法:如果当前元素大于加上步长后的那个元素则交换
if (array[j] > array[j + gap]) {
array[j] = array[j] + array[j + gap];
array[j + gap] = array[j] - array[j + gap];
array[j] = array[j] - array[j + gap];
}
}
}
}
System.err.println(Arrays.toString(array));
}
private static void shellSort2(int[] array) {
int length = array.length;
// 按下标的一定增量分组,增量 gap,并逐步的缩小增量
for (int gap = length / 2; gap > 0; gap /= 2) {
// 遍历所有的分组
for (int i = gap; i < length; i++) {
// 从第 gap 个元素,逐个对其所在的组进行直接插入排序
int insert = array[i];
int insertIndex = i;
// insertIndex - gap >= 0 插入位置不能越界
while (insertIndex - gap >= 0 && insert < array[insertIndex - gap]) {
array[insertIndex] = array[insertIndex - gap];
insertIndex -= gap;
}
// 当退出 while 后,就给 insert 找到插入的位置
array[insertIndex] = insert;
}
}
System.err.println(Arrays.toString(array));
}
}
6.9 快速排序
快速排序(Quicksort)是对冒泡排序的一种改进。
算法思想
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的数据均比另一部分的数据小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
- 从数列中挑出一个元素,称为 “基准”(pivot)。
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边),在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
算法实现
public class QuickSortTest {
public static void main(String[] args) {
int[] array = CreateArrayUtils.createArray(10);
System.err.println(Arrays.toString(array));
quickSort(array, 0, array.length - 1);
System.err.println(Arrays.toString(array));
}
private void quickSort(int[] nums, int left, int right) {
if (left > right) {
return;
}
int pivot = nums[left];
int i = left, j = right;
while (i < j) {
// 从右向左找小于 pivot
while (i < j && nums[j] >= pivot) {
j--;
}
// 从左向右找大于 pivot
while (i < j && nums[i] <= pivot) {
i++;
}
if (i != j) {
nums[i] = nums[i] + nums[j];
nums[j] = nums[i] - nums[j];
nums[i] = nums[i] - nums[j];
}
}
nums[left] = nums[i];
nums[i] = pivot;
quickSort(nums, left, i - 1);
quickSort(nums, i + 1, right);
}
}
6. 10 归并排序
算法思想
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治策略(divide-and-conquer)。分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起即分而治之。
算法描述
- 把长度为n的输入序列分成两个长度为n/2的子序列。
- 对这两个子序列分别采用归并排序。
- 将两个排序好的子序列合并成一个最终的排序序列。
算法实现
public class MergetSort {
public static void main(String[] args) {
int[] array = CreateArrayUtils.createArray(10);
System.err.println(Arrays.toString(array));
int[] ints = mergeSort(array);
System.err.println(Arrays.toString(ints));
}
public static int[] mergeSort(int[] array) {
if (array.length < 2) {
return array;
}
int mid = array.length / 2;
int[] left = Arrays.copyOfRange(array, 0, mid);
int[] right = Arrays.copyOfRange(array, mid, array.length);
return merge(mergeSort(left), mergeSort(right));
}
public static int[] merge(int[] left, int[] right) {
int[] result = new int[left.length + right.length];
for (int index = 0, i = 0, j = 0; index < result.length; index++) {
if (i >= left.length) {
result[index] = right[j++];
} else if (j >= right.length) {
result[index] = left[i++];
} else if (left[i] > right[j]) {
result[index] = right[j++];
} else {
result[index] = left[i++];
}
}
return result;
}
}
6.11 计数排序
算法思想
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中,作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序(Counting sort)是一种稳定的排序算法,计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数,然后根据数组C来将A中的元素排到正确的位置,只能对整数进行排序。
算法描述
- 找出待排序的数组中最大和最小的元素。
- 创建一个长度为(max - min + 1)的数组 bucket,里面的每一个元素初始都置为0。
- 遍历待排序的数组,计算其中的每一个元素出现的次数,例如:i 元素出现了3次,那么bucket [i - min] = 3。
- 累加 bucket 数组,获得元素的排位,从0开始遍历 bucket,bucket[array[i] - min]++。
- 创建一个临时数组T,长度与待排序数组一样,直接从 bucket 里面获取要插入的元素和元素的具体位置,处理过每个元素之后都要把 bucket 里面对应位置的计数减1。
算法实现
public class CountingSortTest {
public static void main(String[] args) {
int[] array = new int[]{7, 39, 14, 29};//CreateArrayUtils.createArray(4);
System.err.println(Arrays.toString(array));
countingSort(array);
}
private static void countingSort(int[] array) {
// 最大最小值初始化
int min = array[0], max = array[0];
// 寻找最大最小值
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
if (array[i] < min) {
min = array[i];
}
}
// 定义一个额外的数组,相当于是一个空桶,可以存放的下标长度是从 min 到 max 的长度大小
int[] bucket = new int[max - min + 1];
// 初始化桶填充,这里全部为0
Arrays.fill(bucket, 0);
// 保证原数组的元素在bucket中都能占据一个位置
for (int i = 0; i < array.length; i++) {
// 计数,例如一个key为i的元素出现了3次,那么bucket[i-min]=3。
bucket[array[i] - min]++;
}
// 数组内容回填
int index = 0, i = 0;
while (index < array.length) {
if (bucket[i] != 0) {
array[index] = i + min;
bucket[i]--;
index++;
} else {
i++;
}
}
System.err.println(Arrays.toString(array));
}
}
6.12 桶排序
桶排序是计数排序的升级版,它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。
算法思想
桶排序 (Bucket sort)的基本思想:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。
算法描述
- 人为设置一个BucketSize,作为每个桶所能放置多少个不同数值(例如当BucketSize==5时,该桶可{1,2,3,4,5}这几种数字,但是容量不限,即可以存放100个3)。
- 定义元素与桶数量的映射函数 (max - min) / bucketSize。
- 遍历输入数据,并根据映射函数实现的对应关系,将属于同一个桶的元素放入对应的桶里去。
- 对每个不是空的桶进行排序,可以使用其它排序方法,也可以递归使用桶排序。
- 从不是空的桶里把排好序的数据拼接起来。
- 如果递归使用桶排序为各个桶排序,则当桶数量为1时要手动减小BucketSize增加下一循环桶的数量,否则会陷入死循环,导致内存溢出。
算法实现
public class BucketSortTest {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(5);
list.add(9);
list.add(15);
list.add(2);
list.add(7);
System.err.println(list.toString());
List<Integer> sort = bucketSort(list, 5);
System.err.println(sort.toString());
}
private static List<Integer> bucketSort(List<Integer> list, int bucketSize) {
if (null == list || list.size() < 2) {
return list;
}
// 数组的容量
int size = list.size();
// 数组最大最小值初始化,用来计算映射函数
int min = list.get(0), max = list.get(0);
// 寻找最大最小值
for (int i = 1; i < size; i++) {
if (list.get(i) > max) {
max = list.get(i);
}
if (list.get(i) < min) {
min = list.get(i);
}
}
List<Integer> result = new ArrayList<>(size);
// 桶数量的映射函数如下(事先设定好的,也可以试着随便用其他的映射关系看看结果),计算出桶的数量
int bucketCount = (max - min) / bucketSize + 1;
// 将整个桶数组用ArrayList表示,每个桶用存放Integer的ArrayList表示
List<ArrayList<Integer>> buckets = new ArrayList<>(bucketCount);
// 初始化桶,把每一个桶都初始化为一个ArrayList<Integer>
for (int i = 0; i < bucketCount; i++) {
buckets.add(new ArrayList<>());
}
// 根据映射函数实现的对应关系,将属于同一个桶的元素放入对应的桶
for (int i = 0; i < size; i++) {
// 映射函数实现元素与桶之间的对应
int item = (list.get(i) - min) / bucketSize;
// 将属于同一个桶的元素放入对应的桶
buckets.get(item).add(list.get(i));
}
// 对每个桶进行排序
for (int i = 0; i < bucketCount; i++) {
// 如果带排序数组中有重复数字时
if (bucketSize == 1) {
List<Integer> arrayList = buckets.get(i);
for (int j = 0; j < arrayList.size(); j++) {
result.add(arrayList.get(j));
}
} else {
if (bucketCount == 1) {
bucketSize--;
}
// 递归对每个桶进行排序,直到每个桶中只有一个元素
List<Integer> temp = bucketSort(buckets.get(i), bucketSize);
for (int j = 0; j < temp.size(); j++) {
result.add(temp.get(j));
}
}
}
return result;
}
}
6.13 基数排序
基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),n 为数组长度,k 为数组中的数的最大的位数。
算法思想
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
算法描述
算法实现