快速排序
快速排序是冒泡排序的改进版,也是最好的一种内排序,还涉及到分治和递归,在很多面试题中都会出现,也是作为程序员必须掌握的一种排序方法。
冒泡排序中记录的比较和交换是在相邻的单元中进行,每次交换只能上移或者下移一个单元,因而总的比较和移动次数较多。
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。
该方法的基本思想是:
1.先从数列中取出一个数作为基准数(简单起见可以取第一个数)。
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。(分区)
3.再对左右区间重复第一步、第二步,直到各区间只有一个数。(递归)
第一趟排序
对待排序序列进行划分的做法是:
使用两个指针 low 和 high 分别指向待划分序列 r 的范围,
取 low 所指元素为枢轴,即 pivot = r[low]。
划分首先从 high 所指位置的元素起向前逐一搜索到第一个比 pivot 小的元素,
并将其设置到 low 所指的位置;
然后从 low 所指位置的元素起向后逐一搜索到第一个比 pivot 大的元素,
并将其设置到 high 所指的位置;
不断重复上述两步直到 low = high 为止,
最后将 pivot 设置到 low 与 high 共同指向的位置。
使用上述划分方法即可将待排序序列按枢轴元素 pivot 分成两个子序列,
当然 pivot 的选择不一定必须是 r[low],而可以是 r[low..high]之间任何数据元素。
快速排序=冒泡+分治+递归 快速排序的过程:东拆西补或西拆东补,一边拆一边补
算法实现代码:
快速排序算法的实现
import java.util.Arrays;
public class TestQuickSort {
/**
* 分区,返回调整后基准数的位置(两个分区的边界)
* @param arr
* @param low
* @param high
* @return
*/
private static int partition(int []arr,int low,int high) {
//指定两个指针,分别执行分区起始元素和结束元素
int i = low;
int j = high;
//指定第一个元素是基准值,挖坑一个
int x = arr[low];
//生成队列
while(i<j){
//右边指针向左移动,直到找到小于基准值的元素
while(i<j && arr[j]>=x){
j--;
}
//使用右边元素填左边坑
if(i<j){
arr[i] = arr[j];
i++;
}
//左边指针向右移动,直到找到小于基准值的元素
while(i<j && arr[i]<x){
i++;
}
//使用左边元素填右边坑
if(i<j){
arr[j] = arr[i];
j--;
}
}
//填最后的坑(基准值应该放的位置)
arr[i] = x;//此时i==j
//返回基准位置
return i;
}
/**
* 快速排序
* @param arr
* @param low
* @param high
*/
private static void quickSort(int[] arr, int low, int high) {
if(low < high){
//分区一次, 前小中基准后面大,返回基准数位置
int n = partition(arr,low,high);
//对左边进行快速排序
quickSort(arr,low,n-1);
//对右边进行快速排序
quickSort(arr,n+1,high);
}
}
public static void quickSort(int[] arr) {
int low = 0;
int high = arr.length-1;
quickSort(arr,low,high);
}
public static void main(String[] args) {
int arr[] = {72,6,57,88,60,42,83,73,48,85};
//int arr[] = {72,83,83,83,83,83,83,73,83,85};
System.out.println(Arrays.toString(arr));
quickSort(arr);
System.out.println(Arrays.toString(arr));
}
}
快速排序算法的分析
1.当分区选取的基准元素为待排序元素中的最大或最小值时,为最坏的情况,时间复杂度和直接插入排序的一样,移动次数达到最大值
Cmax = 1+2+…+(n-1) = n*(n-1)/2 = O(n2) 此时最好时间复杂为O(n2)
2.当分区选取的基准元素为待排序元素中的”中值”,为最好的情况,时间复杂度为O(nlog2n)。
3.快速排序的空间复杂度为O(1*log2n). (用到了递归,当然占用空间多了)
4.当待排序元素类似[6,1,3,7,3]且基准元素为6时,经过分区,形成[1,3,3,6,7],两个3的相对位置发生了改变,所是快速排序是一种不稳定排序。
快速排序算法的分析2
时间效率:快速排序算法的运行时间依赖于划分是否平衡,即根据枢轴元素 pivot 将序列划分为两个子序列中的元素个数,
而划分是否平衡又依赖于所使用的枢轴元素。下面我们在不同的情况下来分析快速排序的渐进时间复杂度。
快速排序的最坏情况是每次进行划分时,在所得到的两个子序列中有一个子序列为空。此时,算法的时间复杂度T(n) = T p (n) + T(n-1),
其中T p (n)是对具有n个元素的序列进行划分所需的时间,由以上划分算法的过程可以得到T p (n) = Θ(n)。由此,T(n) =Θ(n) + T(n-1) =Θ(n 2 )。
在快速排序过程中,如果总是选择r[low]作为枢轴元素,则在待排序序列本身已经有序或逆向有序时,快速排序的时间复杂度为Ο(n 2 ),
而在有序时插入排序的时间复杂度为Ο(n)。
快速排序的最好情况是在每次划分时,都将序列一分为二,正好在序列中间将序列分成长度相等的两个子序列。
此时,算法的时间复杂度T(n) = T p (n) + 2T(n/2),由于T p (n) = Θ(n),所以T(n) = 2T(n/2) +Θ(n),
由master method知道T(n) = Θ(n log n)。
在平均情况下,快速排序的时间复杂度 T(n) = kn ㏑ n,其中 k 为某个常数,经验证明,在所有同数量级的排序方法中,
快速排序的常数因子 k 是最小的。因此就平均时间而言,快速排序被认为是目前最好的一种内部排序方法。
快速排序的平均性能最好,但是,若待排序序列初始时已按关键字有序或基本有序,则快速排序蜕化为起泡排序,其时间复杂度为Ο(n 2 )。
为改进之,可以采取随机选择枢轴元素pivot的方法,具体做法是,在待划分的序列中随机选择一个元素然后与r[low]交换,再将r[low]作为枢轴元素,
作如此改进之后将极大改进快速排序在序列有序或基本有序时的性能,在待排序元素个数n较大时,其运行过程中出现最坏情况的可能性可以认为不存在。
空间效率:虽然从时间上看快速排序的效率优于前述算法,然而从空间上看,在前面讨论的算法中都只需要一个辅助空间,
而快速排序需要一个堆栈来实现递归。若每次划分都将序列均匀分割为长度相近的两个子序列,则堆栈的最大深度为 log n,
但是,在最坏的情况下,堆栈的最大深度为 n。
并发集合
线程安全的集合类
1.早期集合类Vector、Hashtable:线程安全的
是怎么保证线程安排的
使用synchronized修饰方法
2.为了提高性能,重速度轻安排,使用ArrayList、HashMap替换,线程不安全,但是性能好
3.使用ArrayList、HashMap,需要线程安全怎么办呢?
使用
Collections.synchronizedList(list);
Collections.synchronizedMap(m);
解决
底层使用synchronized代码块锁
虽然也是锁住了所有的代码,但是锁在方法里边,并所在方法外边性能可以理解为稍有提高吧。毕竟进方法本身就要分配资源的
4.在大量并发情况下如何提高集合的效率和安全呢?
java.util.concurrent.*
ConcurrentHashMap:
CopyOnWriteArrayList :
CopyOnWriteArraySet: 注意 不是CopyOnWriteHashSet
ConcurrentHashMap: 分段(segment)锁定+Lock锁
HashMap的线程安全班,并且性能比Hashtable、Collections.synchronizedMap(m);都有提高
使用的不是synchronized代码块锁,也不是synchronzied方法锁。
并且使用了锁分离技术,使用多个锁来控制对hash表的不同部分(段segment)进行的修改,采用ReentrantLock锁来实现。
如果多个修改操作发生在不同的段上,他们就可以并发进行,从而提高了效率
JDK1.7和JDK1.8的关于ConcurrentHashMap的实现差异较大,以上理论属于JDK1.7;
ConcurrentHashMap在JDK8中进行了巨大改动。它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS算法。
它底层由“数组”+链表+红黑树的方式思想(JDK8中HashMap的实现),
为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。
CopyOnWriteArrayList :CopyOnWrite+Lock锁
对于set()、add()、remove()等方法使用ReentrantLock的lock和unlock来加锁和解锁
读操作不需要加锁
CopyOnWrite容器即写时复制的容器。
通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,
添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
对于读操作远远多于写操作的应用非常适合,特别在并发情况下,可以提供高性能的并发读取。
CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
CopyOnWriteArraySet:CopyOnWrite+Lock锁
它是线程安全的无序的集合,可以将它理解成线程安全的HashSet。
有意思的是,CopyOnWriteArraySet和HashSet虽然都继承于共同的父类AbstractSet;
但是,HashSet是通过“散列表(HashMap)”实现的,
而CopyOnWriteArraySet则是通过“动态数组(CopyOnWriteArrayList)”实现的,并不是散列表
CopyOnWriteArraySet在CopyOnWriteArrayList 的基础上使用了Java的装饰模式,所以底层是相同的。
而CopyOnWriteArrayList本质是个动态数组队列,所以CopyOnWriteArraySet相当于通过通过动态数组实现的“集合”!
CopyOnWriteArrayList中允许有重复的元素;但是,CopyOnWriteArraySet是一个集合,所以它不能有重复集合。
因此,CopyOnWriteArrayList额外提供了addIfAbsent()和addAllAbsent()这两个添加元素的API,
通过这些API来添加元素时,只有当元素不存在时才执行添加操作!
CopyOnWrite容器即写时复制的容器。
通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,
而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,
添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,
因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
接口增强
/**
* 1.JDK7及其之前,接口中都是抽象方法,且不能出现static方法
* 2.接口的变量都是public final static 全局静态常量,无变化
* 3.接口中可以添加非抽象方法(static),通过接口名调用,不能使用实现类的对象名调用
* 4.接口中可以添加非抽象方法(非static),需使用default定义。
* 实现类可以直接使用default方法,可以重写default方法,但是必须去掉default(default只能接口中使用)
* 5.上级接口中default方法的调用:MyInterface.super.method2();
* 6.目的是为了既有的成千上万的Java类库的类增加新的功能, 且不必对这些类重新进行设计。 *
* 比如, 只需在Collection接口中增加default Stream<E> stream(),
* 相应的Set和List接口以及它们的子类都包含此的方法, 不必为每个子类都重新copy这个方法。
* 但是如果父类、多个接口都同名default、static方法可怎么办呢?有优先顺序和解决之道
*
* 或者是为了解决实现该接口的子类代码重复的问题
* 、
* @author Administrator
*
*Collection :增加一个方法并且实现
*
*List Set
*ArrayList HashSet....
*
*/
public interface MyInterface {
//不能出现static方法
//public static abstract void method1();
//抽象方法,之前就支持
void method1();
//
static void method2(){
System.out.println("JDK8后接口中可以有static修饰的非抽象方法");
}
//private protected public
public default void method3(){
System.out.println("JDK8后接口中可以有非static的非抽象方法,必须添加default");
System.out.println("这个default可不是switch的default,也不是默认权限修饰符");
}
public default void method3(String name){
method3();
System.out.println(name);
}
public static void main(String[] args) {
MyInterface.method2();
}
}
public class MyClass implements MyInterface{
@Override
public void method1() {
System.out.println("MyClass method1");
}
//接口中的default方法,子类可以直接调用,也可以重写,但是必须去掉default
//default是接口中专用的
@Override
public void method3() {
MyInterface.super.method3();//!!! 如何调用接口的default方法
}
public static void main(String[] args) {
MyInterface myClass = new MyClass();
//myClass.method1();
//myClass.method3();
myClass.method3("abc");//default方法只能通过对象名调用
//myClass.method2();//static
MyInterface.method2();//静态方法只能通过接口名调用
//因为一个类可能实现多个接口,万一多个接口中都有static的且非抽象的method2
}
}