目录
预备知识
被排序的对象属于Comparable类型,因此可以使用ComparaTo方法对输入数据施加相容的排序,除(引用)赋值运算外,这是仅有的对输入数据进行操作。在这些条件下的排序叫做基于比较的排序。
排序的稳定性:如果 2 9 3 5 1 3' 10 ,排序完是1 2 3 3' 5 9 10则就是稳定的排序,排序完是1 2 3' 3 5 9 10则就是不稳定的排序。判定方法是稳定的排序没有发生跳跃性的交换。稳定的排序可以变为不稳定的排序,不稳定的排序不能变为稳定的排序。
关于内部排序和外部排序,内部排序是在内存上进行排序,针对于元素个数相对来说比较小(小于几百万)。外部排序是在磁盘上进行排序,针对于元素个数较大的排序。本篇文章前七种属于内部排序,最后一种是外部排序。
冒泡排序
算法
使用两个for循环,第一个for循环是表示趟数i,第二个for循环是表示 比较次数j。相邻元素进行比较,满足条件进行交换。比较次数p之后,会有p个数据有序。
原始数组 | 8 | 6 | 3 | 9 | 4 | 7 | 比较次数 |
第一趟之后 | 6 | 3 | 8 | 4 | 7 | 9 | 5 |
第二趟之后 | 3 | 6 | 4 | 7 | 8 | 9 | 4 |
第三趟之后 | 3 | 4 | 6 | 7 | 8 | 9 | 3 |
第四趟之后 | 3 | 4 | 6 | 7 | 8 | 9 | 2 |
第五趟之后 | 3 | 4 | 6 | 7 | 8 | 9 | 1 |
public static void bubbleSort(int[] array) {
for (int i = 0; i < array.length-1 ; i++) {
for (int j = 0; j < array.length-1-i; j++) {
if (array[j] > array[j+1]) {
int tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
}
}
}
}
冒泡排序的分析
两层for循环,时间复杂度O(N)。
没有申请额外空间,空间复杂度:O(1)
稳定性:稳定的
冒泡排序的优化
我们会发现上面的例子在第三趟的时候已经有序了,因此不用再进行比较了。那么代码怎么知道有序了,是在比较的过程中如果没有进行交换了说明就有序了。我们可以定义一个boolean类型的变量进行如下优化。
public static void bubbleSort(int[] array) {
boolean flg = false;
for (int i = 0; i < array.length-1; i++) {
for (int j = 0; j < array.length-1-i; j++) {
if (array[j] > array[j+1]) {
int tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
flg = true;
}
}
if (!flg) {
break;
}
}
}
选择排序
算法
i从0号下标开始,选择后面比它小的元素和它交换。一趟排序完成第一个元素就有序了。
public static void selectSort(int[] array) {
for (int i = 0; i < array.length; i++) {
for (int j = i+1; j < array.length; j++) {
if (array[i] > array[j]) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
}
}
选择排序分析
时间复杂度:O(n^2)[最好和最坏都是]
空间复杂度:O(1)
稳定性:不稳定
堆排序
算法
把数组通过层序遍历的方式建立一个堆。如果要进行升序排序,要调整为一个大根堆。那么最顶层的元素就是有序的,就是最大的元素。如果进行降序排序,要调整为一个小根堆,那么最顶层的元素就是有序的,就是最小的元素。
具体做法是:
1.要调整为一个大根堆(从小到大排序)。那么就是保证每一颗子树都是大根堆,从上往下调整,即就是向下调整。调整的方式是,让父亲节点和左右孩子的最大值比较(前提是有右孩子),要保证有右孩子,就是判断child+1是否小于length。如果孩子节点大于父亲节点,两者交换即可。【这只是调整了一次】 第二次调整只需要让根节点-1继续进行调整。
2.进行排序,第一个元素和最后一个元素进行交换,最后一个元素就有序了。调整0号下标的那棵树为大根堆。定义一个end,每交换一次end--,进行向下调整。当end=0说明调整完成。
//向下调整
public static void adjustDown(int[] array, int root, int len) {
int parent = root;
int child = 2*parent+1;
while (child < len) {
if (child+1 < len && array[child] < array[child+1]) {
child++;
}
//此时child就是指向子孩子的较大值
if (array[child] > array[parent]) {
int tmp = array[child];
array[child] = array[parent];
array[parent] = tmp;
//调整子树也要是大根堆
parent = child;
child = 2*parent+1;
} else {
break;
}
}
}
//每棵树都向下调整
public static void creatHeap(int[] array) {
for (int i = (array.length-1-1)/2; i >= 0; i--) {
adjustDown(array, i, array.length);
}
}
//进行排序
public static void heapSort(int[] array) {
creatHeap(array);
int end = array.length-1;
while (end > 0) {
int tmp = array[0];
array[0] = array[end];
array[end] = tmp;
//adjustDown取不到len 所以先调整后end--
adjustDown(array, 0, end);
end--;
}
}
public static void main(String[] args) {
int[] array = new int[]{27, 15, 19, 18, 28, 34, 65, 49, 25, 37};
heapSort(array);
System.out.println(Arrays.toString(array));
}
堆排序的分析
时间复杂度:由于根节点和叶子节点个数大概是1:1,选根节点是N/2,每个根节点都要进行向下调整log2N。再加上排序的O(N),即就是log2N(2为底)*N/2 + O(N)== Nlog2N
空间复杂度:O(1)
最好、最坏、平均复杂度都是Nlog2N
建堆的时间复杂度:Nlog2N
一次调整的时间复杂度:log2N
稳定性:不稳定
直接插入排序
算法
排序有N-1趟排序组成,对于p=1到p=N-1,插入排序保证0-p位置上的元素为已排序状态。排序情况如下:
原始数组 | 34 | 8 | 64 | 51 | 32 | 21 |
p=1趟之后 | 8 | 34 | 64 | 51 | 32 | 21 |
p=2趟之后 | 8 | 34 | 64 | 51 | 32 | 21 |
p=3趟之后 | 8 | 34 | 51 | 64 | 32 | 21 |
p=4趟之后 | 8 | 32 | 34 | 51 | 64 | 21 |
p=5趟之后 | 8 | 21 | 32 | 34 | 51 | 64 |
这个排序的做法我们可以类比于揭扑克牌。第一个数据已经有序,i应该从1开始。把i号下标的数据放到tmp。比较i号下标前面的元素,下标为j=i-1。j号下标的元素和tmp相比较。如果j号下标元素比tmp大,把j号下标元素放到j+1号下标,j--,当j比0小时,说明前面没有元素了。再把tmp里的数据放到j+1位置。这是一趟快速排序。要注意,如果tmp比j号元素大,说明前面已经有序,直接把tmp放到j+1。
public static void insertSort(int[] array) {
int j = 0;
for (int i = 1; i < array.length; i++) {
int tmp = array[i];
for (j = i-1; j >= 0; j--) {
if (tmp < array[j]) {
array[j+1] = array[j];
} else {
break;
}
}
array[j+1] = tmp;
}
}
插入排序的分析
有两层for循环,如果输入数据有序,内层循环的检验总是立即判定不成立而终止,所以最好时间复杂度是O(N),最坏的是O(N^2),即就是无序的时候。并且数据越有序越快。
没有申请额外空间,因此空间复杂度是O(1)
该排序是稳定的,若把条件if (tmp < array[j])改为if (tmp <= array[j])该排序就变为不稳定的排序了。
希尔排序
算法
希尔排序直接插入排序的优化。分组进行排序,越有序直接插入排序越快。增量一定互为素数,最后一个增量必须为1。分组我们可以采用跳跃式分组,跳跃式的分组会让比较大的数据在后面,比较小的数据在前面(升序的情况),使得数据更佳具有有序性。
public static void shell(int[] array, int gap) {
for (int i = gap; i < array.length; i++) {
int tmp = array[i];
int j = 0;
for (j = i-gap; j >= 0; j -= gap) {
if (array[j] > tmp) {
array[j+gap] = array[j];
} else {
break;
}
}
array[j+gap] = tmp;
}
}
public static void shellSort(int[] array) {
//分的组数
int[] drr = {5,3,1};
for (int i = 0; i < drr.length; i++) {
shell(array, drr[i]);
}
}
做法,分别进行了5、3、1的分组,随着分组的改变,数据越来越有序。
希尔排序的分析
时间复杂度:O(n^1.3 - n^1.5)
空间复杂度:O(1)
稳定性:不稳定
快速排序
算法
找基准:定义low为0号下标,high为length-1号下标。将low号下标的元素放到tmp中,从后往前找比tmp中元素小的数据,放到low号的位置;从前往后找比tmp元素大的数据,放到high号下标的位置。当low和high相遇了,把tmp放进去。这个地方就是基准的位置。基准的左边都比基准小,基准的右边都比基准大。pivot【一趟快速排序】--》给个区间[low,high],递归找其他的基准!
public static int partion(int[] array, int start, int end) {
int tmp = array[start];
while (start < end) {
while ((start < end) && array[end] >= tmp) { //9 3 2 9 10
end--;
}
if (start >= end) {
array[start] = tmp;
break;
} else {
array[start] = array[end];
}
while ((start < end) && array[start] <= tmp) { //9 3 2 9 10
start++;
}
if (start >= end) {
array[start] = tmp; //
break;
} else {
array[end] = array[start];
}
}
return start;
}
public static void quick(int[] array, int low, int high) {
//递归的终止条件:只有一个元素
if (low >= high) { //=zuo >you
return;
}
//1.写一个函数把待排序序列分为两部分
int pivot = partion(array, low, high); //low和high是局部变量
//开始递归 左右
quick(array, low, pivot-1);
quick(array, pivot+1, high);
}
public static void quickSort(int[] array) {
quick(array, 0, array.length-1);
}
快速排序的分析
时间复杂度:O(nlog2n)
最好时间复杂度:
最坏时间复杂度:O(n^2)[数据有序的情况]
空间复杂度:O(log2n) [左树高度]
稳定性:不稳定
快速排序的优化
优化1:
当快速排序的过程中数据有可能逐渐趋于有序,对于直接插入排序来说,数据越有序越快。O(n)-》n-》小。当在排序的过程中,某个区间的数据量很小时,阈值-》100 用直接插入排序
public class QuickSortbetter1 {
public static int partion(int[] array, int start, int end) {
int tmp = array[start];
while (start < end) {
while ((start < end) && array[end] >= tmp) { //9 3 2 9 10
end--;
}
if (start >= end) {
array[start] = tmp;
break;
} else {
array[start] = array[end];
}
while ((start < end) && array[start] <= tmp) { //9 3 2 9 10
start++;
}
if (start >= end) {
array[start] = tmp; //
break;
} else {
array[end] = array[start];
}
}
return start;
}
public static void insertSort2(int[] array, int low, int high) {
int j = 0;
for (int i = low+1; i <= high; i++) {
int tmp = array[i];
for (j = i-1; j >= low; j--) {
if (array[j] > tmp) {
array[j+1] = array[j];
} else {
break;
}
}
array[j+1] = tmp;
}
}
public static void quick(int[] array, int low, int high) {
//递归的终止条件
if (low >= high) {
return;
}
if (high-low+1 < 100) {
insertSort2(array, low, high);
return;
}
//1.写一个函数把待排序序列分为两部分
int pivot = partion(array, low, high); //low和high是局部变量
//开始递归 左右
quick(array, low, pivot-1);
quick(array, pivot+1, high);
}
public static void quickSortBetter1(int[] array) {
quick(array, 0, array.length-1);
}
优化2:
如果是1 2 3 4 5 6 7 8 9 10 找基准退化为冒泡排序了
分治算法-》快排--》最好的情况就是将待排序序列均匀的分割。
如果想要1 2 3 4 5 6 7 8 9 10 均匀的分割:三数取中法 9/2=4。。取三位数中间的中位数作为基准
即就是每次取待排序序列low和high的中位数,array[mid] <= array[low] <= array[high]。即要的结果就是就是low下标的元素是三个数的中位数。
public class QuickSortBetter2 {
public static int partion(int[] array, int start, int end) {
int tmp = array[start];
while (start < end) {
while ((start < end) && array[end] >= tmp) { //9 3 2 9 10
end--;
}
if (start >= end) {
array[start] = tmp;
break;
} else {
array[start] = array[end];
}
while ((start < end) && array[start] <= tmp) { //9 3 2 9 10
start++;
}
if (start >= end) {
array[start] = tmp; //
break;
} else {
array[end] = array[start];
}
}
return start;
}
public static void swap(int[] array, int low, int high) {
int tmp = array[low];
array[low] = array[high];
array[high] = tmp;
}
public static void ThreeNumOfMiddle(int[] array, int low, int high) {
//array[mid] <= array[low] <= array[high];
int mid = (low+high)/2;
if (array[mid] > array[high]) {
swap(array, mid, high);
}
if (array[mid] > array[low]) {
swap(array, mid, low);
}
if (array[low] > array[high]) {
swap(array, low, high);
}
}
public static void quick(int[] array, int low, int high) {
//递归的终止条件
if (low >= high) {
return;
}
ThreeNumOfMiddle(array, low, high);
//1.写一个函数把待排序序列分为两部分
int pivot = partion(array, low, high); //low和high是局部变量
//开始递归 左右
quick(array, low, pivot-1);
quick(array, pivot+1, high);
}
public static void quickSort(int[] array) {
quick(array, 0, array.length-1);
}
优化3:
聚集相同基准元素法。
快速排序非递归
public static int partion(int[] array, int start, int end) {
int tmp = array[start];
while (start < end) {
while ((start < end) && array[end] >= tmp) { //9 3 2 9 10
end--;
}
if (start >= end) {
array[start] = tmp;
break;
} else {
array[start] = array[end];
}
while ((start < end) && array[start] <= tmp) { //9 3 2 9 10
start++;
}
if (start >= end) {
array[start] = tmp; //
break;
} else {
array[end] = array[start];
}
}
return start;
}
public static void quick(int[] array, int low, int high) {
int pivot = partion(array, low, high);
Stack<Integer> stack = new Stack<>();
if (pivot > low+1) { //左边有两个元素可以入栈
stack.push(low);
stack.push(pivot-1);
}
if (pivot < high-1) { //右边有两个元素可以入栈
stack.push(pivot+1);
stack.push(high);
}
while (!stack.empty()) {
high = stack.pop();
low = stack.pop();
pivot = partion(array, low, high);
if (pivot > low+1) { //左边有两个元素可以入栈
stack.push(low);
stack.push(pivot-1);
}
if (pivot < high-1) { //右边有两个元素可以入栈
stack.push(pivot+1);
stack.push(high);
}
}
}
public static void quickSort(int[] array) {
quick(array, 0, array.length-1);
}
归并排序
算法
归并排序采用了分治的算法。分就是将一些问题分为小的问题进行递归求解,而治的则将分的阶段解得的各答案修补到一起。给定一个待排序序列时,我们应该分为两大步:1.把它划分为一个一个有序的序列 2.进行二路归并(两个有序表合并为一个有序表)。
//合并递归完的数组
public static void merge1(int[] array,int low,int mid,int high) {
int s1 = low;
int s2 = mid+1;
int[] tmpArr = new int[high-low+1]; //需要重新申请一个空间存储数组元素
int i = 0;//tmpArr的数组下标
//当两个归并段都有数据的时候
while (s1 <= mid && s2 <= high) {
//如果是小于,那么就不稳定了
if(array[s1] <= array[s2]) {
tmpArr[i++] = array[s1++];
}else {
tmpArr[i++] = array[s2++];
}
}
//S1还有数据的情况下
while (s1 <= mid) {
tmpArr[i++] = array[s1++];
}
//s2还有数据的情况下
while (s2 <= high) {
tmpArr[i++] = array[s2++];
}
//tmpArr里面存放的是有序的数据
//将tmpArr里面存放的有序的数据,放回到array里面
for (int j = 0; j < tmpArr.length; j++) {
array[low+j] = tmpArr[j];
}
}
//进行递归:分
public static void mergeSortInternal(int[] array,int low,int high) {
if(low >= high) { //递归的终止条件
return;
}
//以mid区分前后两段 分段递归
int mid = (low+high)/2;
mergeSortInternal(array,low,mid);
mergeSortInternal(array,mid+1,high);
//把递归完的数组进行合并
merge1(array,low,mid,high);
}
public static void mergeSort1(int[] array) {
mergeSortInternal(array,0,array.length-1);
}
归并排序的分析
分治下来就像一颗二叉树,时间复杂度O(n*log2n);
重新申请了一块空间,空间复杂度是O(n)
是一个稳定的排序
归并排序非递归
public static void mergeSort(int[] array) {
//进行分组归并
for (int i = 1; i < array.length; i *= 2) {
merge(array,i);
}
}
//gap代表每个归并段的数据
public static void merge(int[] array,int gap) {
int[] tmpArr = new int[array.length];
int k = 0;//下标
int s1 = 0; //第一组的开头
int e1 = s1+gap-1; //第一组的结尾
int s2 = e1+1; //第二组的开头
int e2 = s2+gap-1 < array.length ? s2+gap-1:array.length-1; //第二组的结尾
//两个归并段都有数据
while (s2 < array.length) {
while (s1 <= e1 && s2 <= e2) {
if(array[s1] <= array[s2]) {
tmpArr[k++] = array[s1++];
}else {
tmpArr[k++] = array[s2++];
}
}
while (s1 <= e1) {
tmpArr[k++] = array[s1++];
}
while (s2 <= e2) {
tmpArr[k++] = array[s2++];
}
s1 = e2+1;
e1 = s1+gap-1;
s2 = e1+1;
e2 = s2+gap-1 < array.length ? s2+gap-1:array.length-1;
}
//判断是不是还有一个归并段,且这个归并段一定是s1那个段,直接小于e1可能会越界
while (s1 <= array.length-1) {
tmpArr[k++] = array[s1++];
}
for (int i = 0; i < tmpArr.length; i++) {
array[i] = tmpArr[i];
}
}
不是所有排序都是基于比较的排序,比如计数排序、基数排序,桶排序等。
2019年的最后一天,在这里许个愿望吧。希望我在2020这个美好的一年里,能拿到一个满意的offer,冲鸭白!为了梦想,不断前行。2019再见,2020你好嘻嘻嘻O(∩_∩)O哈哈~