冒泡排序
冒泡排序算法的原理如下:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
/*
* 冒泡排序
* */
import java.util.Arrays;
public class BubbleSort {
public static void main(String[] args) {
int[] arr = { 1, 4, 5, 5, 67, 3, 2, 5, 6, 8, 10 };
// sort1(arr);
sort2(arr);
System.out.println(Arrays.toString(arr));
}
private static void sort2(int[] arr) {
// 冒泡排序(写法二)
for (int i = arr.length; i >= 0; i--) {
for (int j = 0; j < i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
private static void sort1(int[] arr) {
// 冒泡排序(写法一)
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
}
选择排序
思路:
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
import java.util.Arrays;
/*
* 选择排序
* */
public class SelectSort {
public static void main(String[] args) {
int[] arr = { 1, 11, 4, 5, 5, 67, 3, 2, 5, 6, 8, 10 };
// sort1(arr);
sort2(arr);
System.out.println(Arrays.toString(arr));
}
// 写法一(递增排序)
private static void sort2(int[] arr) {
for (int i = arr.length; i > 0; i--) {
int index = 0; // 最大值索引
int max = arr[0]; // 最大值
for (int j = 0; j < i; j++) { // 找到前n-1个数的最大值和索引位置
if (arr[j] > max) {
index = j;
max = arr[j];
}
}
// 把前n-1个数最大值放在数组后面
int temp = arr[i - 1];
arr[i - 1] = max;
arr[index] = temp;
}
}
// 写法二(递增排序)
private static void sort1(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int index = i;
int min = arr[i];
for (int j = i; j < arr.length; j++) {
if (arr[j] < min) {
index = j;
min = arr[j];
}
}
int temp = arr[i];
arr[i] = min;
arr[index] = temp;
}
}
}
插入排序
基本思路:
插入排序是指在待排序的元素中,假设前面n-1(其中n>=2)个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。按照此法对所有元素进行插入,直到整个序列排为有序的过程,称为插入排序
/*
* 插入排序
* */
import java.util.Arrays;
public class InsertSort {
public static void main(String[] args) {
int[] arr = { 1, 11, 4, 5, 5, 67, 3, 2, 5, 6, 8, 10 };
sort(arr);
System.out.println(Arrays.toString(arr));
}
private static void sort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int value = arr[i]; // 拿到每一张牌,即将要插入的数
int lastIndex = i - 1;
// 依次与前面的数进行比较
while (lastIndex > -1 && value <arr[lastIndex]) { // 注意这里下标越界问题
arr[lastIndex + 1] = arr[lastIndex];
lastIndex--;
}
arr[lastIndex + 1] = value; // 将牌插入,即插入元素
}
}
}
希尔排序
希尔排序(Shell’s Sort)是插入排序的一种,又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。
上图来源https://www.cnblogs.com/chengxiao/p/6104371.html
/*
* 希尔排序
* */
import java.util.Arrays;
public class ShellSort {
public static void main(String[] args) {
int[] arr = { 1, 11, 4, 0, 5, 67, 3, 2, 5, 6, 8, 10 };
sort(arr);
System.out.println(Arrays.toString(arr));
}
private static void sort(int[] arr) {
for (int gap = arr.length / 2; gap > 0; gap = gap / 2) { // 设置每一次的增量(增量依次减半)
// 直接插入排序(每组进行插入排序,互不干扰)
for (int i = gap; i < arr.length; i++) {
int target = arr[i];
int j = i - gap;
while (j > -1 && target < arr[j]) {
arr[j + gap] = arr[j];
j -= gap;
}
arr[j + gap] = target;
}
}
}
}
归并排序
归并排字(Merge Sort)算法完全依照了分治模式
- 分解 : 将n个元秦分成各含n / 2个元素的子序列
- 解决 : 对两个子序列递归地排序
- 合并 : 合并两个已排序的子序列以得到排序结果
import java.util.Arrays;
import java.util.Random;
/*
* 归并排序
* */
public class M {
public static void main(String[] args) {
int[] arr = new int[15];
Random rd = new Random();
for (int i = 0; i < arr.length; i++) {
arr[i] = rd.nextInt(20);
}
System.out.println(Arrays.toString(arr));
mergeSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
private static void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = left + ((right - left) >> 1);
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right); // 合并
}
}
private static void merge(int[] arr, int low, int mid, int high) {
int[] helper = Arrays.copyOf(arr, arr.length); // 辅助数组容器(容器可以放在外面,以防止多次创建)
int current = low; // 注意这里需要指向数组当前所划分的区域的最小位置
int left = low;
int right = mid + 1;
while (left <= mid && right <= high) {
if (helper[left]<=helper[right]) {
arr[current] = helper[left];
left++;
}else {
arr[current] = helper[right];
right++;
}
current++;
}
// 这里只考虑左半部分的数是否有还没有放入原数组中,右半部分本身就是较大的数所以会在元素中的末尾部分,无需考虑
while (left<=mid) {
arr[current] = helper[left];
left++;
current++;
}
}
}
快速排序
思想:
- 分解 : 数组A[p…r]被划分为两个子数组A[p…q-1]和A [q+1,r],使得A[q]为大小居中的数,左侧A [p…q-1]中的每个元素都小于等于它,而右侧A [ q+1,r]中的每个元素都大于等于它。其中计算下标q也是划分过程的一部分。
- 解决 : 通过递归调用快速排序,对子数组A[p…q-1]和A [q+1,r]进行排序。
- 合并 : 为子数组都是原址排序的,所以不需要合并,数组A[p…r]已经有序。
关键点:在于划分问题。
单向扫描分区法
一遍扫描法的思路是,用两个指针将数组划分为三个区间,
扫描指针(sp)左边是确认小于等于主元的,
扫描指针到某个指针(next_bigger_pos)中间为未知的
因此我们将第二个指针(next bigger pos)称为未知区间末指针,末指针的右边区间为确认大于主元的元素
import java.util.Arrays;
import java.util.Random;
/*
* 快排:单向扫描分区法
* */
public class Q1 {
public static void main(String[] args) {
// int[] arr = { 9, 10, 7, 8, 3, 6, 3, 11, 2, 1, 4, 5, 13, 12 };
int[] arr = new int[15];
Random rd = new Random();
for (int i = 0; i < arr.length; i++) {
arr[i] = rd.nextInt(20);
}
System.out.println(Arrays.toString(arr));
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
private static void quickSort(int[] arr, int p, int r) {
if (p < r) {
int q = partition(arr, p, r); // 划分,将索引q的左边变为都是小于arr[q]的数,右边变为都是大于arr[q]的数
quickSort(arr, p, q - 1); // 左边排序
quickSort(arr, q + 1, r); // 右边排序
}
}
private static int partition(int[] arr, int p, int r) {
int first = arr[p]; // 将区间的第一个元素作为划分比较数
int sp = p + 1; // 扫描指针,从第二元素开始
int bigger = r; // 最右边的数的索引
while (sp <= bigger) {
if (arr[sp]<first) { // 小于比较数,扫描指针继续右移,数据不变
sp++;
}
else { // 大于等于比较数,先交换sp和bigger的数,然后最右边的指针向左移
swap(arr, sp, bigger);
bigger--;
}
}
swap(arr, p, bigger); // 将比较数(主区数)放在中间划分位置
return bigger;
}
private static void swap(int[] arr, int a, int b) {
// 交换数组中的两个数的位置
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
双向扫描分区法
思路:
- 头尾指针往中间扫描
- 从左找到大于主元的元素
- 从右找到小于等于主元的元素
- 二者交换
- 继续扫描,直到左侧无大元素,右侧无小元素
private static void quickSort(int[] arr, int p, int r) {
if (p < r) {
int q = partition(arr, p, r);
quickSort(arr, p, q - 1);
quickSort(arr, q + 1, r);
}
}
private static int partition(int[] arr, int p, int r) {
int first = arr[p];
int left = p + 1; // 左指针
int right = r; // 右指针
while (left <= right) {
while (left <= right && arr[left] <=first)
// (left <= right):在进行比较的过程中,防止左指针一直右移超过右指针(极端)
left++;
while (left <= right && arr[right] > first)
// (left <= right):在进行比较的过程中,防止右指针一直左移超过左指针
right--;
if (left < right) { // 防止指针溢出,还进行数组索引报错
swap(arr, left, right);
}
}
swap(arr, p, right);
return right;
}
private static void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
三指针分区法
有相同元素值的快速排序
private static void quickSort(int[] arr, int p, int r) {
if (p < r) {
int[] q = partition(arr, p, r);
quickSort(arr, p, q[0] - 1);
quickSort(arr, q[1] + 1, r);
}
}
private static int[] partition(int[] arr, int p, int r) {
int first = arr[p];
int e = p + 1; // 区分等于
int s = p + 1; // 区分小于
int bigger = r; // 区分大于
while (s <= bigger) {
if (arr[s] < first) {
swap(arr, s, e);
e++;
s++;
} else if (arr[s] == first) {
s++;
} else {
swap(arr, s, bigger);
bigger--;
}
}
swap(arr, --e, p);
int[] q = { e, bigger }; // 相等数的区间
return q;
}
private static void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
快排在工程实践中的优化
三点中值法
绝对中值法
待排序列表较短时,用插入排序
堆排序
在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:
- 最大堆调整(Max Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
- 创建最大堆(Build Max Heap):将堆中的所有数据重新排序
- 堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算
import java.util.Arrays;
/*
* 堆排序(大顶堆)
* */
public class HeapSort1 {
public static void main(String[] args) {
int[] arr = { 3, 4, 9, 10, 1, 5, 2 };
// bigHeapify(arr, arr.length, 0);
// bigHeap(arr, arr.length);
bigHeapSort(arr, arr.length);
System.out.println(Arrays.toString(arr));
}
// 堆排序
public static void bigHeapSort(int[] tree, int n) {
bigHeap(tree, n); // 先堆化一次
for (int i = n - 1; i >= 0; i--) { // 依次将顶部的最大元素放到后面
swap(tree, i, 0);
bigHeapify(tree, i, 0);
}
}
// 这里才构建实现了大顶堆的堆化(将最大的数弄到了顶部)
public static void bigHeap(int[] tree, int n) { // n是节点数
for (int i = (n - 1 - 1) / 2; i >= 0; i--) { // (n-1-1)/2是求最后一个父节点的位置
bigHeapify(tree, n, i);
}
}
// 堆化
public static void bigHeapify(int[] tree, int n, int i) { // n是数的节点数(即数组的长度),i是将要堆化的父节点的索引
if (i >= n) { // 这里其实要不要都可以
return;
}
// 找出父节点的两个子节点的索引
int c1 = i * 2 + 1;
int c2 = i * 2 + 2;
int max = i; // 找出最大数的索引(默认父节点为最大的)
if (c1 <= n - 1 && tree[c1] > tree[max]) { // c1 <= n - 1是判断子节点是否存在
max = c1;
}
if (c2 <= n - 1 && tree[c2] > tree[max]) { // c2 <= n - 1是判断子节点是否存在
max = c2;
}
if (max != i) {
swap(tree, i, max);
bigHeapify(tree, n, max); // 这里要保证交换后的子节点对应的值是 以其为父后面的所有节点的最大值
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
计数排序
- 一句话:用辅助数组对数组中出现的数字计数,元素转下标,下标转元素
- 假设元素均大于等于0 ,依次扫描原数组,将元素值k记录在辅助数组的k位上
- 依次扫描辅助数组,如果为1 ,将其插入目标数组的空白处
时间复杂度:扫描一次source,扫描一次helper,复杂度为N+k
空间复杂度:辅助空间k,k=maxOf(source)
非原址排序
稳定性:相同元素不会出现交叉,非原址都是拷来拷去
如果要优化一下空间,可以求minOf(source),helper的长度位max-min+1,这样能短点
适用范围数据较为密集或范围较小时,适用。
import java.util.Arrays;
public class T5 {
public static void main(String[] args) {
int[] arr = { 3, 2, 9, 0, 1, 99, 2 };
countSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void countSort(int[] arr) {
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int i : arr) {
if (max < i) {
max = i;
}
if (min > i) {
min = i;
}
}
int helperLen = max - min + 1; // 优化了辅助数组空间的浪费
int[] helper = new int[helperLen];
for (int i : arr) {
helper[i - min]++;
}
int index = 0; // 用来当做辅助数组的指针,标记位置
for (int i = 0; i < arr.length; i++) {
while(index<helperLen && helper[index]<=0) { //不断进行找到下一个值的位置
index++;
}
arr[i] = index+min;
helper[index]--;
}
}
}
桶排序
工作的原理:
将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。
- 人为设置一个bucketSize,作为每个桶所能放置多少个不同数值(例如当BucketSize==5时,该桶可以存放{1,2,3,4,5}这几种不同的数字,但是容量不限,即可以存放100个3);
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序,可以使用其它排序方法,也可以递归使用桶排序;
- 从不是空的桶里把排好序的数据拼接起来。
例如:bucketSize = 5 ,数组={ 10, 11, 4, 2, 3, 1, 7, 6, 1 }
即桶的数量= (max - min) / bucketSize + 1 = 3
然后计算每一个元素在几号桶中:桶号 = (元素- min) / bucketSize
桶序号 | 桶中元素 |
---|---|
0 | 1, 1, 2, 3, 4 |
1 | 6, 7, 10 |
2 | 11 |
//构造链表的节点(类)
public class LinkedNode {
int val;
LinkedNode next;
public LinkedNode(int val) {
this.val = val;
}
}
import java.util.Arrays;
/*
* 桶排序
* */
public class BucketSort {
public static void main(String[] args) {
int[] arr = { 10, 11, 4, 2, 3, 1, 7, 6, 1 };
bucketSort(arr, 5);
System.out.println(Arrays.toString(arr));
}
/*
* arr : 数组 bucketSize : 认为设置的每一个桶中可以放的不同种数值
*/
private static void bucketSort(int[] arr, int bucketSize) {
// 找到最大值和最小值
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (int i : arr) {
if (i < min) {
min = i;
}
if (i > max) {
max = i;
}
}
int bucketCount = (max - min) / bucketSize + 1; // 桶的数量
// 构造桶
LinkedNode[] buckets = new LinkedNode[bucketCount];
// 入桶
for (int i : arr) {
int index = (i - min) / bucketSize; // 计算出每一个元素入桶的索引位置(即进入几号桶)
if (buckets[index] == null) { // 空桶
buckets[index] = new LinkedNode(i);
} else {
// 将元素节点插入到相应位置(使桶是有序的)
LinkedNode node = new LinkedNode(i);
LinkedNode head = buckets[index];
insertLink(head, node);
}
}
// 出桶
int i = 0;
for (LinkedNode node : buckets) {
while (node != null) {
arr[i] = node.val;
i++;
node = node.next;
}
}
}
// 将元素节点插入链表中
private static void insertLink(LinkedNode head, LinkedNode node) {
LinkedNode pre = head;
while (head != null) {
if (node.val < head.val) {
LinkedNode nextNode = head.next;
int value = head.val;
head.val = node.val;
head.next = node;
node.val = value;
node.next = nextNode;
return;
}
pre = head;
head = head.next;
}
pre.next = node;
}
}
基数排序
Radix Sort
思路:是一种特殊的桶排序
初始化0-9号十个桶
一、按个位数字,将关键字入桶,入完后,依次遍历10个桶,按检出顺序回填到数组中,如
321 322 331 500 423 476 926
0 | 500 |
---|---|
1 | 321 331 |
2 | 322 |
3 | 423 |
4 | 无 |
5 | 无 |
6 | 476 926 |
7 | 无 |
8 | 无 |
9 | 无 |
检出后数组序列为: 500 321 331 423 476 926,然后取十位数字重复过程一,得到
0 | 500 |
---|---|
1 | 无 |
2 | 321 423 926 |
3 | 331 |
4 | 无 |
5 | 无 |
6 | 476 |
7 | 无 |
8 | 无 |
9 | 无 |
检出后数组序列为: 500 321 423 926 331 476,然后取百位数字重复过程一,得到
0 | 无 |
---|---|
1 | 无 |
2 | 无 |
3 | 321 331 |
4 | 423 476 |
5 | 500 |
6 | 无 |
7 | 无 |
8 | 无 |
9 | 926 |
检出后数组序列为: 321 331 423 476 500 926,已然有序
但是我们应该继续入桶,不过因为再高位全部是0了,这些元素会按顺序全部进入0号桶,这时0号桶的size==数组的size,这时结束标志
最后再回填到数组,数组就是升序排列的了
时间复杂度: 假设最大的数有k位,就要进行k次入桶和回填,每次入桶和回填是线性的,所以整体复杂度为kN,
其中k为最大数的10进制位数
空间复杂度:桶是10个,10个桶里面存n个元素,这些空间都是额外开辟的,所以额外的空间是N+k,k是进制
肯定是非原址的了
稳定性:假设有相等的元素,它们会次第入桶,次第回数组,不会交叉,所以是稳定的
import java.util.ArrayList;
import java.util.Arrays;
/*
* 基数排序
* */
public class RadixSort {
public static void main(String[] args) {
int[] arr = {321, 322, 331, 500, 423, 476, 926};
System.out.println(Arrays.toString(arr));
radixSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void radixSort(int[] arr) {
int max = arr[0]; // 找出最大值
for (int i : arr) {
if (max < i) {
max = i;
}
}
int n = 1; // 代表的一个整数是几位数(默认是1位数)
while (max / 10 != 0) {
n++;
max /= 10;
}
// 创建了0~9个桶且为ArrayList类型
ArrayList[] bucket = new ArrayList[10];
for (int i = 0; i < bucket.length; i++) {
bucket[i] = new ArrayList();
}
for (int i = 0; i < n; i++) {
distribute(arr, i + 1, bucket); // 分配
collect(arr, bucket); // 收集
}
}
private static void collect(int[] arr, ArrayList[] bucket) {
int k = 0;
for (int i = 0; i < bucket.length; i++) {
for (Object j : bucket[i]) {
arr[k++] = (int) j;
}
}
// 清空所有的列表
for (ArrayList arrayList : bucket) {
arrayList.clear();
}
}
private static void distribute(int[] arr, int n, ArrayList[] bucket) {
for (int i : arr) {
int j = (int) (i % (Math.pow(10, n)) / (Math.pow(10, n - 1))); // 计算个位(或者十位等等)所在的数的值
bucket[j].add(i); // 将对应的数值的元素添加到对应列表中
}
}
}