冒泡排序(Bubble Sort)
- 执行流程
① 从头开始比较每一对相邻元素,如果第1个比第2个大,就交换它们的位置;✓ 执行完一轮后,最末尾那个元素就是最大的元素
② 忽略 ① 中曾经找到的最大元素,重复执行步骤 ①,直到全部元素有序。
private static void bubbleSort(int[] arr) {
// 边界条件判断(注:细节问题,安全编码)
if (arr==null || arr.length<2) return;
for (int end = arr.length - 1; end > 0; end--) {
for (int begin = 1; begin <= end ; begin++) {
if (arr[begin] < arr[begin - 1]){
int tmp = arr[begin];
arr[begin] = arr[begin - 1];
arr[begin - 1] = tmp;
}
}
}
ArrayUtil.print(arr);
}
另一个版本理解思路
//N个数字冒泡排序,总共要进行N-1趟比较,每趟的排序次数为(N-i)次比较
private static void bubbleSort(int[] arr) {
// 边界条件判断(注:细节问题,安全编码)
if (arr==null || arr.length<2) return;
// N个数字要排序完成,总共进行N-1趟排序,每i趟的排序次数为(N-i)次
// 外层 i 控制循环多少趟,内层 j 控制每一趟的循环次数
for (int i = 1; i < arr.length; i++){
for (int j = 0; j < arr.length - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
ArrayUtil.print(arr);
}
- 优化-1- 如果序列已经完全有序,可以提前终止冒泡
private static void bubbleSortOptimization1(int[] arr) {
// 边界条件判断(注:细节问题,安全编码)
if (arr==null || arr.length<2) return;
// **优化点** 引入标记值flag 如果在某一次内循环排序比较时,未发生位置交换,说明排序已经完成,无序排序,则提前break 终止循环
for (int end = arr.length - 1; end > 0; end--) {
boolean flag = true;
for (int begin = 1; begin <= end ; begin++) {
if (arr[begin] < arr[begin - 1]){
int tmp = arr[begin];
arr[begin] = arr[begin - 1];
arr[begin - 1] = tmp;
flag = false;
}
}
if (flag) break;
}
ArrayUtil.print(arr);
}
- 优化-2- 如果序列尾部已经局部有序,可以记录最后1次交换的位置,减少比较次
private static void bubbleSortOptimization2(int[] arr) {
//边界条件判断(注:细节问题,安全编码)
if (arr == null || arr.length < 2) return;
// **优化点** 引入位置标记值swagIndex,记录某次循环最后1次交换的位置,如果序列尾部已经局部有序,减少比较次数
for (int end = arr.length - 1; end > 0; end--) {
int swagIndex = 1;//此处数值的确定只要小于等于1即可
for (int begin = 1; begin <= end ; begin++) {
if (arr[begin] < arr[begin - 1]){
int tmp = arr[begin];
arr[begin] = arr[begin - 1];
arr[begin - 1] = tmp;
swagIndex = begin;
}
}
end = swagIndex;
}
ArrayUtil.print(arr);
}
完整代码 >>> 冒泡排序
选择排序(Selection Sort)
◼ 执行流程
① 从序列中找出最大的那个元素,然后与最末尾的元素交换位置
✓ 执行完一轮后,最末尾的那个元素就是最大的元素
② 忽略 ① 中曾经找到的最大元素,重复执行步骤 ①
private static void baseSelectionSort(int[] arr) {
for (int end = arr.length - 1; end > 0 ; end--) {
int maxIndex = 0;
for (int begin = 1; begin <= end; begin++) {
if (arr[begin] > arr[maxIndex]){
maxIndex = begin;
}
}
ArrayUtil.swap(arr,maxIndex,end);
}
ArrayUtil.print(arr);
}
◼ 选择排序的交换次数要远远少于冒泡排序,平均性能优于冒泡排序
◼ 最好、最坏、平均时间复杂度:O(n2),空间复杂度:O(1),属于不稳定排序
堆排序(Heap Sort)
堆排序可以认为是对选择排序的一种优化
◼ 执行流程
① 对序列进行原地建堆(heapify)
② 重复执行以下操作,直到堆的元素数量为 1
✓ 交换堆顶元素与尾元素
✓ 堆的元素数量减 1
✓ 对 0 位置进行 1 次 siftDown 操作
private static void baseHeapSort(int[] arr) {
// 原地建堆
heapSize = arr.length;
for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
siftDown(arr,i);
}
while (heapSize > 1) {
// 交换堆顶元素和尾部元素
ArrayUtil.swap(arr,0, --heapSize);
// 对0位置进行siftDown(恢复堆的性质)
siftDown(arr,0);
}
ArrayUtil.print(arr);
}
private static void siftDown(int[] array, int index) {
int element = array[index];
int half = heapSize >> 1;
while (index < half) { // index必须是非叶子节点
// 默认是左边跟父节点比
int childIndex = (index << 1) + 1;
int child = array[childIndex];
int rightIndex = childIndex + 1;
// 右子节点比左子节点大
if (rightIndex < heapSize &&
ArrayUtil.cmp(array[rightIndex], child) > 0) {
child = array[childIndex = rightIndex];
}
// 大于等于子节点
if (ArrayUtil.cmp(element, child) >= 0) break;
array[index] = child;
index = childIndex;
}
array[index] = element;
}
◼ 最好、最坏、平均时间复杂度:O(nlogn),空间复杂度:O(1),属于不稳定排序
插入排序(Insertion Sort)
◼ 执行流程
① 在执行过程中,插入排序会将序列分为2部分
✓ 头部是已经排好序的,尾部是待排序的
② 从头开始扫描每一个元素
✓ 每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据依然保持有序
private static void baseInsertionSort(int[] arr) {
for (int begin = 1; begin < arr.length; begin++) {
int currentIndex = begin;
while (currentIndex > 0 && arr[currentIndex] < arr[currentIndex - 1]){
ArrayUtil.swap(arr,currentIndex,--currentIndex);
}
}
ArrayUtil.print(arr);
}
逆序对(Inversion)
- 数组 <2,3,8,6,1> 的逆序对为:<2,1> <3,1> <8,1> <8,6> <6,1>,共5个逆序对
◼ 插入排序的时间复杂度与逆序对的数量成正比关系
逆序对的数量越多,插入排序的时间复杂度越高
◼ 最坏、平均时间复杂度:O(n2)
◼ 最好时间复杂度:O(n)
◼ 空间复杂度:O(1)
◼ 属于稳定排序
◼ 当逆序对的数量极少时,插入排序的效率特别高
甚至速度比 O nlogn 级别的快速排序还要快
◼ 数据量不是特别大的时候,插入排序的效率也是非常好的
- 优化-1- 将【交换】转为【挪动】
◼ 思路
① 先将待插入的元素备份
② 头部有序数据中比待插入元素大的,都朝尾部方向挪动1个位置
③ 将待插入元素放到最终的合适位置
private static void optimizationInsertionSort(int[] arr) {
for (int begin = 1; begin < arr.length; begin++) {
int currentIndex = begin;
int currentValue = arr[begin];
while (currentIndex > 0 && arr[currentIndex] < arr[currentIndex - 1]) {
arr[currentIndex] = arr[currentIndex - 1];
currentIndex--;
}
arr[currentIndex] = currentValue;
}
}
归并排序(Merge Sort)
归并排序是采用分治法的一个非常典型的应用。
归并排序的思想就是先递归分解数组,再合并数组。
思路
先考虑合并俩个有序数组,基本思路是比较俩个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直到一个数组为空,最后把另一个数组的剩余部分复制过来即可。
再考虑递归分解,基本思路是将数组分解成left和right,如果这俩个数组内部数据是有序的,那么就可以用上面合并数组的方法将这俩个数组合并排序。如何让这俩个数组内部是有序的?可以再鹅肉粉,知道分解出的小组只含有一个元素时为止,此时认为该小组内部已有序。然后合并排序相邻二个小组即可。
- 执行流程
① 不断地将当前序列平均分割成2个子序列
✓ 直到不能再分割(序列中只剩1个元素)
② 不断地将2个子序列合并成一个有序序列
✓ 直到最终只剩下1个有序序列
- * 将待排序序列R[0...n-1]看成是n个长度为1的有序序列,将相邻的有序表成对归并,得到n/2个长度为2的有序表;将这些有序序列再次归并,
- * 得到n/4个长度为4的有序序列;如此反复进行下去,最后得到一个长度为n的有序序列。
- * 综上可知:归并排序其实要做两件事:(1)“分解”——将序列每次折半划分。(2)“合并”——将划分后的序列段两两合并后排序。
public class Merge_Sort extends Sort{
private static int[] leftArray;
private static int[] array = arr;
public static void main(String[] args) {
//准备一个新的数组,用来备份需要交换的子数组序列
leftArray = new int[array.length >> 1];
ArrayUtil.print(leftArray);
mergeSort(array);
ArrayUtil.print(array);
}
private static void mergeSort(int[] array) {
sort(0,array.length);
}
private static void sort(int begin, int end) {
//排序合法性校验(必须,否则会在递归过程中出现 StackOverflowError)
if ((end - begin) < 2 ) return;
int mid = (begin + end) >> 1;
sort(begin, mid);
sort(mid, end);
merge(begin,mid,end);
}
private static void merge(int begin, int mid, int end) {
//左边数组子序列
int li = 0, le = mid - begin;
//右边数组子序列
int ri = mid, re = end;
//填充指针
int ai = begin;
// 备份左边数组
for (int i = li; i < le; i++) {
leftArray[i] = array[begin + i];
}
// 如果左边还没有结束
while (li < le) {
if (ri < re && (array[ri] - leftArray[li]) < 0) {
array[ai++] = array[ri++];
} else {
array[ai++] = leftArray[li++];
}
}
}
}
快速排序(Quick Sort)
快速排序通常明显比同为Ο(n log n)的其他算法更快,而且快排采用了分治法的思想,因此常被采用。
① 从序列中选择一个轴点元素(pivot)
✓ 假设每次选择 0 位置的元素为轴点元素
② 利用 pivot 将序列分割成 2 个子序列
✓ 将小于 pivot 的元素放在pivot前面(左侧)
✓ 将大于 pivot 的元素放在pivot后面(右侧)
✓ 等于pivot的元素放哪边都可以
③ 对子序列进行 ① ② 操作
✓ 直到不能再分割(子序列中只剩下1个元素)
- 从数列中挑出一个元素作为基准数。
- 分区过程,将比基准数大的放到右边,小于或等于它的数都放到左边。
- 再对左右区间递归执行第二步,直至各区间只有一个数。
public class Quick_Sort extends Sort {
public static void main(String[] args) {
ArrayUtil.print(arr);
sort(0,arr.length);
ArrayUtil.print(arr);
}
private static void sort(int begin, int end){
//排序数组合法性校验
if ((end - begin) < 2) return ;
int mid = pivotIndex(begin, end);
sort(begin, mid);
sort(mid + 1, end);
}
private static int pivotIndex(int begin, int end) {
//选取索引为0即数组的第一个元素作为轴点 pivot
//备份轴点元素
int pivot = arr[begin];
// end指向最后一个元素
end--;
while(begin < end){
//将小于 pivot 的元素放在pivot前面(左侧)
while(begin < end){
if (pivot < arr[end]){
end--;
}else{
arr[begin++] = arr[end];
break;
}
}
//将大于 pivot 的元素放在pivot后面(右侧)
while(begin < end){
if (arr[begin] < pivot){
begin++;
}else{
arr[end--] = arr[begin];
break;
}
}
}
//将pivot放在基准位置
arr[begin] = pivot;
return begin;
}
}
希尔排序(Shell Sort)
希尔排序靶序列看做是一个矩阵,分成m列,逐列进行排序
- m从某个证书逐渐减为1
- 当m为1是,整个序列将完全有序
因此,希尔排序,也称递减增量排序算法,实质是插入排序的优化,即分组插入排序。属于非稳定排序算法。
矩阵的列数取决于步长序列(step sequence),比如步长序列为{1,2,4,8,16,32……},就代表依次分成32列、16列、8列、4列、1列进行排序。
希尔本人给出的步长序列是 𝑛/2𝑘,比如 𝑛 为16时,步长序列是{1, 2, 4, 8}
算法思想
将数组列在一个表中并对列分别进行插入排序,重复这个过程,不过每次用更长的列(步长大,列数少)来进行。最后整个表就只有一列了。将数组转换至表示为了更好地理解这个算法。算法本身还是使用数组进行排序。
public class Shell_Sort extends Sort {
public static void main(String[] args) {
shellSort(arr);
ArrayUtil.print(arr);
}
private static void shellSort(int[] arr) {
//定义步长参数
int step;
int temp,i,j;
for(step = arr.length/2;step > 0;step /= 2){ //增量为len/2 len/4 len/8.....1
for(i=step;i<arr.length;i++){ //对每个子序列进行插入排序
temp = arr[i];
for(j=i;j>=step;j -= step){
if(temp < arr[j-step]){
arr[j] = arr[j-step];
}
else
break;
}
arr[j] = temp;
}
}
}
}
目前已知的最好的步长序列,最坏情况时间复杂度是 O(n4/3) ,1986年由Robert Sedgewick提出
private List<Integer> sedgewickStepSequence() {
List<Integer> stepSequence = new LinkedList<>();
int k = 0, step = 0;
while (true) {
if (k % 2 == 0) {
int pow = (int) Math.pow(2, k >> 1);
step = 1 + 9 * (pow * pow - pow);
} else {
int pow1 = (int) Math.pow(2, (k - 1) >> 1);
int pow2 = (int) Math.pow(2, (k + 1) >> 1);
step = 1 + 8 * pow1 * pow2 - 6 * pow2;
}
if (step >= array.length) break;
stepSequence.add(0, step);
k++;
}
return stepSequence;
}
——————————————
计数排序(Counting Sort)
算法思想
统计每个整数在序列中出现的次数,进而推导出每个整数在有序序列中的索引
public class Counting_Sort extends Sort {
public static void main(String[] args) {
//基础版
baseCountingSort();
ArrayUtil.print(arr);
}
/**
* 基础版本
*/
private static void baseCountingSort() {
//找出数组中最大的元素
int max = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i] > max) max = arr[i];
}
//开辟内存空间,存储每个整数出现的次数
int[] counts = new int[max + 1];
//统计每个整数出现的次数
for (int i = 0; i < arr.length; i++) {
counts[arr[i]]++;
}
//根据整数出现的次数,对原数组进行排序填充
int index = 0;
for (int i = 0; i < counts.length; i++) {
while(counts[i]-- > 0){
arr[index++] = i;
}
}
}
}
简单实现的这种方案,存在无法对负整数进行排序、及其浪费内存空间、是个不稳定的排序。
优化
改进思路
◼ 假设array中的最小值是 min
◼ array中的元素 k 对应的 counts 索引是 k – min
◼ array中的元素 k 在有序序列中的索引 counts[k – min] – p p 代表着是倒数第几个 k
◼ 比如元素 8 在有序序列中的索引 counts[8 – 3] – 1,结果为 7
◼ 倒数第 1 个元素 7 在有序序列中的索引 counts[7 – 3] – 1,结果为 6
◼ 倒数第 2 个元素 7 在有序序列中的索引 counts[7 – 3] – 2,结果为 5
// 找出最值
int max = array[0];
int min = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
if (array[i] < min) {
min = array[i];
}
}
// 开辟内存空间,存储次数
int[] counts = new int[max - min + 1];
// 统计每个整数出现的次数
for (int i = 0; i < array.length; i++) {
counts[array[i] - min]++;
}
// 累加次数
for (int i = 1; i < counts.length; i++) {
counts[i] += counts[i - 1];
}
// 从后往前遍历元素,将它放到有序数组中的合适位置
int[] newArray = new int[array.length];
for (int i = array.length - 1; i >= 0; i--) {
newArray[--counts[array[i] - min]] = array[i];
}
// 将有序数组赋值到array
for (int i = 0; i < newArray.length; i++) {
array[i] = newArray[i];
}
◼ 最好、最坏、平均时间复杂度:O(n + k)
◼ 空间复杂度:O(n + k)
◼ k 是整数的取值范围
◼ 属于稳定排序
基数排序(Radix Sort)
基数排序非常适合用于整数排序(尤其是非负整数),因此本课程只演示对非负整数进行基数排序
◼ 执行流程:依次对个位数、十位数、百位数、千位数、万位数...进行排序(从低位到高位)
◼ 最好、最坏、平均时间复杂度:O(d ∗ (n + k)) ,d 是最大值的位数,k 是进制。属于稳定排序
◼ 空间复杂度:O(n + k),k 是进制
public class Radix_Sort extends Sort {
public static void main(String[] args) {
RadixSort();
}
protected static void RadixSort() {
// 找出最大值
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
// 个位数: arr[i] / 1 % 10 = 3
// 十位数:arr[i] / 10 % 10 = 9
// 百位数:arr[i] / 100 % 10 = 5
// 千位数:arr[i] / 1000 % 10 = ...
for (int divider = 1; divider <= max; divider *= 10) {
countingSort(divider);
}
}
protected static void countingSort(int divider) {
// 开辟内存空间,存储次数
int[] counts = new int[10];
// 统计每个整数出现的次数
for (int i = 0; i < arr.length; i++) {
counts[arr[i] / divider % 10]++;
}
// 累加次数
for (int i = 1; i < counts.length; i++) {
counts[i] += counts[i - 1];
}
// 从后往前遍历元素,将它放到有序数组中的合适位置
int[] newarr = new int[arr.length];
for (int i = arr.length - 1; i >= 0; i--) {
newarr[--counts[arr[i] / divider % 10]] = arr[i];
}
// 将有序数组赋值到arr
for (int i = 0; i < newarr.length; i++) {
arr[i] = newarr[i];
}
}
}
桶排序(Bucket Sort)
执行流程
① 创建一定数量的桶(比如用数组、链表作为桶)
② 按照一定的规则(不同类型的数据,规则不同),将序列中的元素均匀分配到对应的桶
③ 分别对每个桶进行单独排序
④ 将所有非空桶的元素合并成有序序列
◼ 元素在桶中的索引
元素值 * 元素数量