java插入算法的优化_排序系列及其拓展优化(Java实现)

一、插入排序

1. 算法思想:设一共有n个元素,对于第i轮排序,在第i到第n个元素中找到最大值x,将x放在第i个位置。

2. 时间复杂度: 要执行n轮排序,每次以O(n)时间寻找最值,时间复杂度O(n2)

3. 空间复杂度: 不需要开辟额外空间 O(1)

4. 优点:简单

5. 缺点:时间复杂度过高,最好情况和最坏情况都是O(n2)

6. 代码描述

packagesort;importjava.util.Random;public classExample {//选择排序

public static voidselectSort(Comparable[] a) {for(int i=0; i

Comparable min= a[i];//记录该轮排序的最小值

int pos = i;//记录最小值的位置

for(int j=i+1; j

pos=j;

min=a[j];

}

}

exch(a, i, pos);//对a数组交换第i和第pos元素的位置

}

}//交换函数,交换a数组中i,j两位置

public static void exch(Comparable[] a, int i, intj) {

Comparable t=a[i];

a[i]=a[j];

a[j]=t;

}//打印元素的值//如果是对象要重写一下toString方法

public static voidshow(Comparable[] a) {for(Comparable t : a)

System.out.print(t.toString()+ " ");

System.out.println();

}//比较函数 可以自定义比较关键字//注意,该对象需要实现Comparable接口并且重写compareTo方法

private static intcmp(Comparable a, Comparable b) {//TODO Auto-generated method stub

returna.compareTo(b);

}public static voidmain(String[] args) {//TODO Auto-generated method stub

Random random = new Random(10);

Comparable[] a= new Integer[100];//Integer内部实现了Comparable接口

for(int i=0; i<100; i++) {

a[i]= random.nextInt(1000);

}

show(a);//排序前

selectSort(a);

show(a);//排序后

}

}

二、 插入排序

1. 算法思想:

(1) 设一共有n个元素,对于第i轮排序,前i-1个元素已经排好序,那么寻找在第1个到第i-1个元素中寻找一个位置将第i个元素插进去,类似于打扑克。

很显然,如果找到一个插入位置x∈[1,i-1],那么设t=a[i] 从第x到第i-1所有元素都要向后移一位,把第x位置空出来后再插入第i个元素t

(2) 上面的方法可以改进使得代码比较简单,设t=a[i],既然需要后移,可以每一步都将t与左边元素的值比较,如果左边的大就与左边元素互换位置,直到左边的元素不大于t,省去挪位置。

2. 时间复杂度:

(1)最坏情况下即逆序情况下,每一轮插入值都需要插到最前面,复杂度O(n2)

(2) 如果元素基本有序,复杂度接近O(n)

3. 空间复杂度: 不需要开辟额外空间 O(1)

4. 优点:简单,适合基本有序的数组的排序,时间复杂度接近线性

5. 缺点:依赖数组初始情况,最坏情况是O(n2)

6. 代码描述

//插入排序

public static voidinsertSort(Comparable[] a) {for(int i=1; i

for(int j=i; j>0; j--) {//寻找插入位置

if(cmp(a[j] , a[j-1]) >= 0) {//发现前面的数比它小,那说明位置已经找到了,不需要再找

break;

}else {//否则不断与相邻元素交换位置

exch(a , j, j-1);

}

}

}

}

三、希尔排序

1. 算法思想:

(1) 首先介绍什么是h有序数组,一个数组是h有序数组,则这个数组可以拆分成h个互不相交的间隔一致为h的有序子数组(相对顺序不变),比如

2  3  19  5 4  21  10  8  48  12  66  57

可以拆分成h = 3的有序子数组

2        5          10           12

3         4           8              66

19        21          48           57

(2)希尔排序的思想就是是数组中任意间隔为h的元素都是有序的。h选取一个数列,逐步迭代直至为1,那么这个数组就是全部有序的了。

(3)不难看出,可以在插入排序的基础上,对每一个h数组进行实现。

2. 时间复杂度:

根据选取的h序列(递增序列)不同,复杂度不一样,而且难以计算,只能评估一个下限,希尔斯排序复杂度O(N4/3)O(N5/4)等等

3. 空间复杂度: 不需要开辟额外空间 O(1)

4. 优点:克服了插入排序需要频繁交换的问题,适合大规模排序,原因是平衡了子数组的规模和有序性。

5. 缺点:速度没有特别快。

6. 代码描述

//希尔排序

public static voidshellSort(Comparable[] a ) {//构造h

int h = 1;int n =a.length;while(h <= n/3) h = h*3+1;//h = 1 4 13 40 121...

while(h >= 1) { //每一轮使原数组变成h有序数组

for(int i=h; i

for(int j=i; j>=h && cmp(a[j] , a[j-h]) < 0; j-=h) { // exch(a , j , j-h); //}

}

h= h/3;//h收缩,直至1,此时的h有序数组就是有序数组

}

}

四、归并排序

1. 算法思想:

(1) 假如有一个数组,左半边是有序的,右半边也是有序的,那么如何把它们整体变为有序呢?

(2) 很显然,只要对左右两边每次挑一个较小的出来,直到左边挑完或者右边挑完,把剩余部分直接接在已经排好序的数列的尾部就可以了。

4aa124c8a061a4d544d574881c5579ff.png

2. 时间复杂度:

(1)需要二分logN次,每次归并是线性复杂度,总体是O(NlogN)

3. 空间复杂度: 需要开辟额外空间 O(N),用于辅助数组aux, 函数递归调用的栈空间O(logN),总体空间复杂度O(N)

4. 优点:速度快

5. 缺点:空间复杂度高

6. 代码描述

//归并排序

private staticComparable[] aux;//merge方法

public static void merge(Comparable[] a, int lo, int mid , inthi) {int p = lo, q = mid+1;for(int i=lo; i<=hi; i++) aux[i] =a[i];int cnt =lo;while( p <= mid && q <=hi) {if(cmp(aux[p] , aux[q]) < 0) a[cnt++] = aux[p++];else a[cnt++] = aux[q++];

}while(p <= mid) a[cnt++] = aux[p++];while(q <= hi) a[cnt++] = aux[q++];

}//sort方法

public static void sort(Comparable[] a, int lo, inthi) {if(lo >= hi) return ; //如果low > high则返回

int mid = lo + ((hi - lo) >> 1);//取中间点为中点

sort(a , lo , mid);//对左边的归并

sort(a , mid + 1, hi);//对右边的归并

merge(a , lo , mid , hi);//左右归并好了则归并到一起

}//启动函数

public static voidmergeSort(Comparable[] a) {

aux= newComparable[a.length];

sort(a ,0, a.length - 1);

}

【小插曲】

一开始运行一直报栈溢出的错误,指示在sort方法中 sort(a , lo , mid);这一行,debug时发现有时候mid比lo还小,说明是mid运算那一行出错了,

一开始  int mid = lo + (hi - lo) >> 1; 后来查到两点关于移位操作的知识

(1) 原本右移一位等价于将整数除以2,现在发现对于负奇数不成立,原因是移位和除法截断方式不一样,

比如 -7 / 2 = -3  ,但-7 >> 1 = -4

(2) 移位优先级比加减要低,此处错误源于这一点

【拓展】自顶向下与自底向上的归并排序

(1)事实上,上述方法是自顶向下分解原数组到每组1个数,然后调用merge回溯

(2)其实也可以首先将原数组分解,分组大小从1到2到4到2i , 自底向上归并。只需要修改sort函数

//from bottom to up

public static voidmergeSortBU(Comparable[] a) {int n =a.length;

aux= newComparable[n];for(int sz = 1; sz < n; sz +=sz) {for(int lo = 0; lo < n - sz + 1; lo +=sz+sz) {

merge(a , lo, lo+sz-1, Math.min(lo+sz+sz-1 , n-1));

}

}

}

五、快速排序

1. 算法思想

(1)快速排序的主要思想和归并排序类似,都是分治,都是左边部分有序,右边部分有序,再整体有序。但是快速排序的切分点是不稳定的,并不总是中点。

(2)对于快速排序,其切分策略是,总是找当前范围内第一个数x应该处于的位置k,即a[lo]~a[k-1]小于等于x,a[k+1]~a[hi]都大于等于x,找位置K的方法是,从两头往中间夹逼,

途中如果发现一对左边大于x的数和右边小于x的数便将它们交换,直到两头碰到为止。这样一来就实现了对于位置K而言,左边的数都不比他大,右边的数都不比他小。

(3)分别对a[lo]~a[k-1]和a[k+1]~a[hi]递归地使用(1)(2),可以证明每个数都“各得其所”,数组也就整体有序了。

cb4780c07fa421bf332173bf5d9d72be.png

2. 时间复杂度:O(NlogN)

3. 空间复杂度: 不需要开辟额外空间 O(1),但是需要额外的平均栈空间O(logN),最多退化到O(N)。

4. 优点:速度快,时间复杂度和空间复杂度 都很低

5. 缺点:比较脆弱,可能退化到冒泡,当每轮排序总是使左边1个数右边其他数的时候。

6. 代码描述

//切分函数

private static int partition(Comparable[] a, int lo, inthi) {

Comparable v=a[lo];int i = lo, j = hi;//i,j分别是左部分指针,右部分指针

while(true) {//左边要找一个比他大的,否则一直找知道i指针到终点hi

while(i<=hi)if(cmp(a[++i] , v) <= 0); else break;//右边要找一个比他小的,否则一直找到j指针到起点lo

while(j>lo)if(cmp(a[j] , v) >= 0) j--; else break;//如果ij指针相遇 则说明已经找到a[lo]的位置,

if(i>=j) break;//这里等号应该取不到因为上面设置的是严格大和严格小,不加等号也 是对的

exch(a , i , j);//交换位置,把不正确的位置纠正过来,把左边大的与右边小的元素位置互换

}

exch(a , lo , j);//最后把第lo个元素放到该 放到的地方

return j;//返回该轮排序的切分点

}//递归排序

private static void qsort(Comparable[] a, int lo, inthi) {//TODO Auto-generated method stub

if(lo >= hi) return;int mid =partition(a , lo, hi);

qsort(a , lo, mid-1);

qsort(a, mid+1, hi);

}//快速排序启动函数

public static voidquickSort(Comparable[] a) {

qsort( a,0, a.length - 1);

}

【拓展】三向切分的快速排序

(1)当数组中存在大量相同关键字的元素时,快速排序对其处理的性能损失在大量切分再排序,一个有效的处理方法是使用三向切分的快速排序。

(2)主要思想:每轮排序时,将lo到hi的元素分割成三个部分使得[lo, lt - 1] < V  = [lt , gt]

93e164b2a1f84ffbec8b26e106f963f4.png

(3)三向切分快排代码描述

//三向切分快速排序

public static void qsort3(Comparable[] a, int lo, inthi) {if(lo >= hi) return;//lo lt gt hi四个指针//事实上,从lt一开始指向关键字a[lo],lt就一直指向的值是V,并且lt指的是//第一个V值,它存的其实是左边界//排好序后lt ~ gt都是V值

int lt = lo, i = lo+1 , gt =hi;

Comparable v=a[lo];while(i <=gt ) {int re =cmp(a[i] , v);if(re < 0) exch(a , i++ ,lt++);else if( re > 0) exch(a , i, gt--);else i++;

}//只需要再排不等于V的两个部分

qsort3(a , lo, lt - 1);

qsort3(a , gt+1, hi);

}

六、 优先队列(基于数组和堆)

1. 作用:优先队列使得队列首部元素永远是最大值(最小值),并且只能在队首操作(要么查看队首元素要么弹出队首元素),

可以新插入元素并将其插入合适位置使得结构还是优先队列。适用于构造动态变化的优先级结构。

2. 什么是堆?

(1)这里只介绍二叉堆 。任意K叉堆可以类比得出。以下简称二叉堆为堆。

(2)二叉堆是一组按照完全二叉树的结构排列的元素,其特点是,如果某一结点在数组中标号为k并且有孩子结点,那么其孩子结点标号是2k, 2k+1(如果有有孩子的话),

且满足,a[k] >= max(a[2k] , a[2k+1])  这样的二叉堆叫大根堆,同理有小根堆。

19ec838ea2fbbc55dcf11914d2eb5eab.png

(3)二叉堆中调整元素结构保持大根堆的特性的核心操作有两个,上浮和下沉,都是为了保持结构为堆

//上浮第K号元素

public void swim(intk) {//当前位置是k,则父节点位置为k/2

while(k > 1 && less(k >> 1, k)) {

exch(k>> 1, k);//交换二者位置

k = k >> 1;//继续检验,设置当前位置为父节点位置

}

}//下沉第K号元素

public void sink(intk) {while(2*k

int j = 2*k;//j指针指向左右孩子中较大的一个

if(less(j , j+1)) j++;//如果当前结点比左右孩子中较大的一个小则交换,父节点下沉

if(less(k , j)) {

exch(k , j);

k=j;

}else{break;

}

}

}

3.完整的数据结构如下代码所示

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

packagedata_structure;importjava.util.Random;/*优先队列*/

public class MaxPQ >{private Key[] pq;//容器 数组

private int N;//末尾指针, N=0不指向元素

private intmax;//构造函数

public MaxPQ(intmaxN){

pq= (Key[]) new Comparable[maxN + 1];

N= 0;

max= maxN + 1;

}//判断空

public booleanisEmpty() {return N == 0;

}//返回大小

public intsize() {returnN;

}//插入操作

public voidinsert(Key x) {//TODO 如果当前size已经超过分配的容量则追加空间

if(N + 1 >=max) {

Key[] temp= (Key[]) new Comparable[max * 3 / 2];

max= max * 3 / 2;

System.arraycopy(pq,0, temp, 0, N+1);for(int i=1; i<=N; i++) System.out.print(pq[i].toString() + " ");

System.out.println();

pq= temp;//pq重新指向temp

temp = null;//销毁temp

}

pq[++N] = x;//先将元素插到末尾,再通过上浮调整结构

if(pq[N] == null) System.out.println(123);

swim(N);

}//删除操作

public booleandelMax() {//如果当前队列已经空了则返回失败

if(N == 0) return false;//先把顶头元素与末尾元素互换

exch(1, N);//然后把末尾元素删掉

N--;

pq[N+1] = null;//释放空间//再调整顶头元素位置

sink(1);return true;

}public boolean less(int i, intj) {if(pq[i] == null) System.out.println(123);return pq[i].compareTo(pq[j]) < 0;

}public void exch(int i, intj) {

Key t= pq[i]; pq[i] = pq[j]; pq[j] =t;

}//上浮元素

public void swim(intk) {//当前位置是k,则父节点位置为k/2

while(k > 1 && less(k >> 1, k)) {

exch(k>> 1, k);//交换二者位置

k = k >> 1;//继续检验,设置当前位置为父节点位置

}

}public void sink(intk) {while(2*k

int j = 2*k;//j指针指向左右孩子中较大的一个

if(less(j , j+1)) j++;//如果当前结点比左右孩子中较大的一个小则交换,父节点下沉

if(less(k , j)) {

exch(k , j);

k=j;

}else{break;

}

}

}publicKey top() {return pq[1];

}public static voidmain(String[] args) {//TODO Auto-generated method stub

MaxPQ mpq = new MaxPQ(10);

Random r= newRandom();for(int i=0; i<100; i++) {

Integer t= new Integer(r.nextInt(1000));

mpq.insert(t);

System.out.println(mpq.top().toString());

}

}

}

View Code

【拓展】基于大根堆和sink操作的堆排序

(1)首先给定一个乱序数组,长度为N,将其构造成一个大根堆。

(2)构造方法是,对前N/2号元素逆序(即N/2   N/2 - 1   N/2 - 2   ....   1 )做sink操作,其实就是最底下一层元素不动,逐级向上构造局部大根堆,使得整体成为大根堆。

(3)得到一个大根堆之后,首项即为最大元素,它的位置理应在最后面,所以将其a[1] 与 a[N]交换,并且使a[1] ~ a[N--]的元素保持为大根堆,只需要对新首项做sink操作即可。

这样操作N次之后,每次得到一个仅次于之前元素的最大值排在后面,相当于是选择排序,这样就能使数组整体有序啦。

(4)时间复杂度:N/2 * log(N) + N*log(N) ,整体为O(Nlog(N))

(5)代码实现

//堆排序 , 注意数组a从下标1开始,0号元素没有用

public static voidheapSort(Comparable[] a) {int N = a.length - 1;//N是最后一个元素下标

for(int k = N/2; k>0; k--) {

sink(a , k, N);

}while(N > 1) {

exch(a ,1, N--);

sink(a ,1, N);

}

}//一定要注意第三个参数N是当前选定的最后一个位置, 并不一直是数组末尾元素

private static void sink(Comparable[] a, int k, intN) {while(2*k <=N) {int j = 2*k;if(j < N && cmp(a[j] , a[j+1]) < 0) {

j++;

}if(cmp(a[k] , a[j]) >= 0) {break;

}

exch(a , k , j);

k=j;

}

}

---恢复内容结束---

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值