(PPT来源:尚硅谷数据结构)
速度测试:
80000个随机数字排序
冒泡排序:18s
选择排序:3s
插入排序:5s
希尔排序:交换法17s && 移动法 1s
快速排序:1s
归并排序:<1s
基数排序:1s
插入排序
简单插入排序
初表看作只有首元素有序,从后面 n-1 个元素中依次取出元素,插入有序部分的适当位置
public static void insertSort(int[] arr){
for(int i = 1; i < arr.length; i++){
int insertVal = arr[i]; //待插入的数字
int insertIndex = i-1; //假设的待插入下标
//在下标不越界的情况下,一步步向前寻找当前元素能插入的正确位置
while(insertIndex >= 0 && insertVal < arr[insertIndex]){
//抹去了当前元素在数组中的值
arr[insertIndex + 1] = arr[insertIndex];
insertIndex--;
}
//退出循环,说明待插入的位置找到了
arr[insertIndex + 1] = insertVal;
}
System.out.println(Arrays.toString(arr));
}
1. 直接排序
适用于基本有序,数据量不大的排序表
从后向前查找应插入的位置,比较元素的次数为 n-1
时间复杂度:O( n² )
稳定性:√
2. 折半插入查找
减少了比较元素的次数,为 nlogn
时间复杂度:O( n² )
稳定性:√
3. 希尔排序
简单插入排序时,最坏情况下移动次数过于多:
优化:缩小增量
将待排序表分割成 d1 个特殊的子表(所有距离为d1倍的放在一起),进行插入排序;再分割成更小的 d2 个特殊的子表插入排序;最后分别有序后再整体进行一次插入排序——一般d1=n/2;d2=d1/2…
时间复杂度:特定范围O(n^1.3),最坏情况为O( n² )
稳定性:×
public static void shellSort移动法(int[] arr){
int len = arr.length;
int d = len / 2;
while(d > 0){
for(int i = d; i < len; i++){
int j = i; //保存待插入位置的下标
int tmp = arr[j]; //保存待插入元素的值
//插入排序
if(arr[j-d] > arr[j]){
while(j-d >= 0 && arr[j-d] > tmp){
arr[j] = arr[j-d];
j -= d;
}
//退出while循环即找到了j适当的插入位置
arr[j] = tmp;
}
}
d /= 2;
}
System.out.println(Arrays.toString(arr));
}
交换排序
1. 冒泡排序
两指针从前向后,两两比较,依据大小交换顺序
- 一趟确定一个元素的正确的位置(正序确定最大/逆序确定最小);
每趟排序次数逐渐减小;
最坏情况一共进行 n-1 次循环
优化:如果在一趟排序中,一次都没有交换过,说明已经有序 - 平均时间复杂度:O( n² )
稳定性:√
public static void BubbleSort(int[] arr){
int n = arr.length;
int tmp = 0; //数组交换位置时的临时变量
for(int i = 0; i < n-1; i++){ //总共进行n-1次
boolean flag = false;
for(int j = 0; j < n-1-i; j++){ //n-1-i 即此趟需要排序的个数
//如果前面的数 > 后面的数,交换
if(arr[j] > arr[j+1]){
tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
flag = true;
}
}
if(flag = false){ //在一趟排序中若没有交换过数据,说明已经有序
return;
}
}
}
2. 快速排序
对冒泡排序的改进
做法:前找找,后找找~ ~ ~
将待排序数列中任选一个元素作为基准 pivot,一趟排序后会将基准数放在其正确的位置上;之后再递归对左右子表进行快速排序
平均时间复杂度:最坏情况蜕变为冒泡排序,O(n²)
稳定性:×
- 中间值作为基准
public class 快速排序 {
public static void main(String[] args) {
int[] m = new int[]{3,7,8,0,9,2,5,4};
quickSort(m, 0, m.length-1);
System.out.println(Arrays.toString(m));
}
public static void quickSort(int[] arr, int left, int right){
int l = left;
int r = right;
int mid = l + (r - l)/2;
int pivot = arr[mid];
while(l < r){
//直到找到一个需要交换的左值、右值
while(arr[l] < pivot){
l++;
}
while(arr[r] > pivot){
r--;
}
if(l >= r){ //左右值已经平衡
break;
}
int tmp = arr[l];
arr[l] = arr[r];
arr[r] = tmp;
//当左值/右值寻找完后,则只用再次寻找另一边
if(arr[l] == pivot){
r--;
}
if(arr[r] == pivot){
l--;
}
if(l==r){
l++;
r--;
}
if(left < r){
quickSort(arr, left, r);
}
if(l < right){
quickSort(arr, l, right);
}
}
}
}
- 第一个数作为基准(劣质分割,主要看思想)
import java.util.*;
public class Finder { //快速排序思路,第k大的数字即排序后index=k-1的数字
public int findKth(int[] a, int n, int K) {
// write code here
quickSort(a, 0, n-1);
return a[n-K]; //从小到大排列,所以要从后向前查看选择第k个数
}
public void quickSort(int[] a, int start, int end){
if(start < end){ //递归
int i = partition(a, start, end); //寻找中心基准数,并开始一趟排序
quickSort(a, i+1, end);
quickSort(a, start, i-1);
}
}
public int partition(int[] a, int start, int end){
int x = a[start];
int i = start;
for(int j = start+1; j <= end; j++){
if(a[j] < x){
swap(a, i+1, j);
i++;
}
}
swap(a, start, i); //将基准数放在正确的位置上
return i; //返回基准数后,以其为界限划分处两个子列表进行快速排序
}
public void swap(int[] a, int i, int j){
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
}
选择排序
1. 简单排序
排列好前 i 个元素后,每次从 i 之后挑选最小的元素放到 i + 1 的位置
- 每轮排序都是一个寻找最小元素的循环;
寻找最小元素:先假定当前数最小,依次与后续元素比较,有更小的,就重新确定最小数的值与下标;
一共需要 n - 1 轮排序; - 平均时间复杂度:O( n² )
稳定性:×
public static void selectSort(int[] arr){
int n = arr.length;
for(int i = 0; i < n-1; i++){
//每轮都寻找最小元素
int index = i;
int min = arr[i];
for(int j = i+1; j <= n-1; j++){
//重置最小值
if(min > arr[j]){
index = j;
min = arr[j];
}
}
//优化:当 下标 变换,说明当前数不是最小数时,才需要交换;
//当前数字碰巧为最小数时不需要交换
if(index != i){
arr[index] = arr[i];
arr[i] = min;
}
}
System.out.println(Arrays.toString(arr));
}
2. 堆排序
- 堆:
- 如何将堆调整为小根堆?
输出堆顶元素,以堆中最后一个元素代替之 → 筛选过程:从堆顶至叶子调整,即将新根节点与左右孩子进行比较,小的交换上去 → 反复筛选
元素较多时有效率,建堆操作
平均时间复杂度:O( nlogn )
稳定性:×
归并排序(多路归并 是外部排序)
分 + 治:将两个或两个以上的有序列表组合成一个新的有序表
时间复杂度线性增长
eg:2-路归并排序
public class 归并排序 {
public static void main(String[] args) {
int[] arr = new int[]{3,7,8,0,9,2,5,4};
int[] tmp = new int[arr.length];
mergeSort(arr, 0, arr.length-1, tmp);
System.out.println(Arrays.toString(arr));
}
public static void mergeSort(int[] arr, int left, int right, int[] tmp) {
if(left < right){
int mid = (left + right)/2;
//向左右递归进行分解
mergeSort(arr, left, mid, tmp);
mergeSort(arr, mid+1, right, tmp);
merge(arr, left, mid, right, tmp);
}
}
public static void merge(int[] arr, int left, int mid, int right, int[] tmp){
int i = left;
int j = mid + 1;
int t = 0;
//左右两边已经有序,按照规则拷贝到 tmp 中
while(i <= mid && j <= right){
//判断两个分组的各个元素大小,按照大小拷贝到 tmp
if(arr[i] <= arr[j]){
tmp[t++] = arr[i];
i++;
}else{
tmp[t++] = arr[j];
j++;
}
}
//左边分组还有剩余
if(i <= mid){
tmp[t++] = arr[i++];
}
if(j <= right){
tmp[t++] = arr[j++];
}
//并不是每次都将tmp的所有元素拷贝到arr
t = 0;
int tmpLeft = left;
while(tmpLeft <= right){
arr[tmpLeft++] = tmp[t++];
}
}
}
时间复杂度:O(nlogn)
稳定性:√
基数排序
public class 基数排序 {
public static void main(String[] args) {
int[] arr = {53, 3, 542, 748, 14, 214};
RadixSort(arr);
}
public static void RadixSort(int[] arr){
int max = arr[0]; //数组中最大数
for(int i = 0; i < arr.length; i++){
if(arr[i] > max){
max = arr[i];
}
}
int maxLength = (max + "").length(); //最大数的位数,决定了需要循环几轮
int[][] bucket = new int[10][arr.length];
int[] bucketElementCounts = new int[10]; //各个桶中放入数据的个数
for(int i = 0, n = 1; i < maxLength; i++, n*=10){
//输入元素
for(int j = 0; j < arr.length; j++){
int digitOfElement = arr[j] / n % 10;
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
bucketElementCounts[digitOfElement]++;
}
//取出元素
int index = 0;
for(int k = 0; k < 10; k++){
if(bucketElementCounts[k] != 0){
for(int l = 0; l < bucketElementCounts[k]; l++){
arr[index++] = bucket[k][l];
}
}
bucketElementCounts[k] = 0; //取完值后将桶内元素数置0
}
}
System.out.println(Arrays.toString(arr));
}
}
时间复杂度:d 趟分配和收集 O(d(n+r))
稳定性:√