一 排序
参考:十大经典排序算法
排序算法的时间复杂的和空间复杂度:
稳定:若a=b,且a在b的前面,经过排序后a仍在b的前面,则成该排序算法稳定。
稳定的排序:插入排序,冒泡排序,归并排序
不稳定的排序:堆排序,快速排序,选择排序,希尔排序
比较排序:数据之间的排序依赖它们之间的比较。
非比较排序:根据数据在空间中的位置进行排序,对数据规模和数据分布有一定要求。
- 一个稳定的排序,可以实现为不稳定的排序(更改一个等号的问题)
- 一个不稳定的排序,不可能实现为稳定的排序
1 直接插入排序(稳定)
拿到无序数组的第一个元素,来和前面有序数组的元素进行比较,比该元素大的,直接向后移动,进而找到一个合适的位置进行插入。
static void insertSort(int[] array){
//假定第一个元素有序了,从第二个元素开始和前面有序的数据
//进行比较,找位置插入
for (int i =1 ; i <array.length ; i++) {
int ch=array[i];
//这整个循环是一个挪位的过程
int j=0;
for (j = i-1; j >=0; j--) {
if(ch<array[j]){
array[j+1]=array[j];
}else{
break;
}
}
//插入
array[j+1]=ch;
}
}
时间复杂度:
最佳(数据有序时):O(n)
最坏(数据逆序时):O(n2)
平均:O(n2)
空间复杂度: O(1)
注意:
数据越有序,插入排序的时间效率越高。
2 希尔排序(不稳定)
希尔(shell)排序,是插入排序的一种优化,主要是根据“序列越有序,插入排序的时间效率越高”这一原则改进。即通过将数据进行分组插入排序,分组内元素距离的大小称为增量,希尔增量(希尔增量不是最优的增量)的大小可通过:gap1=length/2; gap2=gap1/2;…;gapN=1.进行确定。最后一个增量必须为1.
代码形式上和插入排序相同。
static void shellSort(int[] array,int gap){
for (int i = gap; i < array.length; i++) {
int ch = array[i];
int j=0;
for (j = i-gap; j >=0 ; j-=gap) {
if(ch<array[j]){
array[j+gap]=array[j];
}else{
break;
}
}
//插入
array[j+gap]=ch;
}
}
public static void main(String[] args) {
int[] a={1,6,3,4,9,4,3,2,8};
int[] gap={4,2,1};
for (int i = 0; i < gap.length; i++) {
shellSort(a,i);
}
System.out.println(Arrays.toString(a));
}
时间复杂度:
O(n^1.3-1.5)
空间复杂度:
O(1)
3 选择排序(不稳定)
依次从无序区间中取值,和前面有序区间的最后一个元素进行比较,若无序区间的值小,则进行交换,然后再继续从无序区间中取值比较。
代码连续两个循环搞定:
public void selectSort(int[] array){
for (int i = 0; i < array.length ; i++) {
for(int j=i+1;j<array.length;j++){
if(array[j] < array[i]) {
int tmp = array[j];
array[j] = array[i];
array[i] = tmp;
}
}
}
}
时间复杂度:
最佳:O(n2)
最坏:O(n2)
平均:O(n2)
空间复杂度:
O(1)
4 堆排序(不稳定)
前置知识点了解:完全二叉树中父节点和子节点的关系:
leftChild = 2parent+1;
rightChild = 2parent +2;
parent = (child-1)/2
升序排列数组要建大堆
降序排列数组要建小堆
本例中我们采用升序排序,建一个大堆。
array.length-1-1:
获取最后一个子节点的父节点
创建一个大堆:
将这个数组array看作是完全二叉树层序遍历的结果
每颗子树都要调整为大堆
首先要找到最后一个子节点的父节点(array.length-1-1)/2
//1 创建一个大堆
public void createHeap(int[] array){
//找到最后一个节点的父节点,依次向下调整
for(int i=(array.length-1-1)/2;i>=0;i--){
adjustDown(array,i,array.length);
}
}
向下调整:
public void adjustDown(int[] array,int root,int len){
//时间复杂度log2n
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
parent = child;
child=2*parent+1;
}else{
break;
}
}
}
接下来简单描述一下为啥要向下调整:
以建一个大堆为例:
第一次调整,让parent指向最后一个子节点的父节点。
对于已经是大堆的子树,直接break掉了。
当出现这种情况时,就能够清楚向下调整的意义所在了。
通过向下调整,被改变的子树,重新恢复为大堆,整个大堆建造过程结束。
创建完堆之后进行堆排序,因为是大堆,头节点保存的是最大的元素。堆排序就是将堆顶元素不断向后面的有序数组前添加,这就有点类似于选择排序。
/2 堆排序(默认为大堆),从小到大,第一个和最后一个交换
public void heapSort(int[] array){
//传入一个数组,先建立一个大堆
createHeap(array);
int end = array.length-1;
while(end>0){
//将堆顶元素和end元素进行交换
int tmp = array[end];
array[end] = array[0];
array[0] = tmp;
//end=array.length-1,此处的end代表数组长度
//交换之后,通过向下调整,重新建立一个大根堆
adjustDown(array,0,end);
end--;
}
}
注意:
在向下调整时,传的是数组的大小adjustDown(array,0,end);
,而堆排序里面的end指的是数组的下标,所以要在 end--;
之前进行向下调整adjustDown(array,0,end);
。
时间复杂度:
最佳:O(nlog2n)
最坏:O(nlog2n)
平均:O(nlog2n)
建堆的时间复杂度为:O(nlog2n)
空间复杂度:
O(1)
5 冒泡排序(稳定)
通过相邻数的比较将最大的数冒泡到无序区间的最后。
public static void bubbleSort(int[] array){
//外层for循环控制冒泡的次数,每一次冒泡的完成代表,
//完成一个数字有序排序
for (int i = 0; i <array.length-1 ; i++) {
//内层for循环控制每次冒泡进行交换的次数
//如果在一次冒泡的过程中,一次都没有进行交换,则说明
//该数组已有序,退出冒泡排序即可
boolean flg = false;
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;
}
}
}
时间复杂度:
数据有序时 O(n)
数据逆序时 O(n^2)
空间复杂度:
O(1)
6 快速排序(不稳定)
- 从待排序区间选择一个数,作为基准值(pivot)
- partition:遍历整个待排序区间,将比基准值小的放到基准值的左边,比基准值大的放到基准值的右边
- 采用分治思想,对分好的左右区间以同样的方式进行处理,直到小区间长度==1.
public void quickSort(int[] array){
quick(array,0,array.length-1);
}
public void quick(int[] array,int low, int high){
if(low>=high){
return;
}
//依据递归的思想,每次都是找基准点,划分区间
int pivot = partion(array,low,high);
quick(array,low,pivot-1);
//如果pivot为最后一个元素,此时pivot+1将会有low大于high的情况
quick(array,pivot+1,high);
}
public int partion(int[] array,int low,int high){
int tmp =array[low];
while(low<high){
//右边找小于tmp的元素,赋给array[low]
while((low<high)&&array[high]>=tmp){
high--;
}
if(low>=high){
break;
}else{
array[low] = array[high];
}
//左边找大于tmp的元素,赋给array[high]
while((low<high)&&array[low]<=tmp){
low++;
}
if(low>=high){
break;
}else{
array[high] = array[low];
}
}
array[low] = tmp;
return low;
}
时间复杂度:
O(nlog2n)
数据有序: O(n^2)
空间复杂度:
O(log2n)
优化方法:
- 设定区间阈值,当待排序区间长度小于该阈值时,直接采用直接插入排序
- 优化选取基准值,三数取中法
设定区间阈值:
// 7.1 快速排序优化1——设置阈值,如果区间长度小于该阈值,则用直接插入排序
//因为,区间越有序,直接插入排序性能越高,为O(n)
public void quickSort1(int[] array){
quick1(array,0,array.length-1);
}
public void quick1(int[] array,int low,int high){
if(low>=high){
return;
}
//区间长度小于该阈值,则用直接插入排序
if(high-low+1<100){
insertSort(array,low,high);
return;
}
int pivot = partion(array,low ,high);
quick1(array,low,pivot-1);
quick1(array,pivot+1,high);
}
public void insertSort(int[] array,int low ,int high){
for (int i = low+1; i <=high ; i++) {
int tmp = array[i];
int j =0;
for ( j =i-1; j >=low; j--) {
if(array[j]>tmp){
array[j+1]=array[j];
}else{
break;
}
}
array[j+1] = tmp;
}
}
三数取中:
// 7.2 快速排序优化2--三数取中。 在区间已经趋于有序的情况下,也是快排最坏的情况
//时间复杂度为O(n^2),
public void quickSort2(int[] array){
quick2(array,0,array.length-1);
}
public void quick2(int[] array,int low,int high){
if(low>=high){
return;
}
ThreeNumOfMiddle(array,low,high);
int pivot = partion(array,low ,high);
quick1(array,low,pivot-1);
quick1(array,pivot+1,high);
}
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;
//array[mid] <= array[high]
if(array[mid] > array[high]) {
swap(array,mid,high);
}
//array[mid] <= array[low]
if(array[mid] > array[low]) {
swap(array,mid,low);
}
//array[low] <= array[high]
if(array[low] > array[high]) {
swap(array,low,high);
}
}
快速排序的非递归形式:
// 8 快速排序的非递归形式
public void quickSort3(int[] array) {
quick3(array,0,array.length-1);
}
public void quick3(int[] array,int low,int high) {
int pivot = partion(array,low,high);
Stack<Integer> stack = new Stack<>();
//>low+1保证左边有两个元素以上,并将左区间入栈
if(pivot > low+1 ) {
stack.push(low);
stack.push(pivot-1);
}
//<high-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);
}
}
}