diff --git "a/Part1/Android/Android\345\206\205\345\255\230\346\263\204\346\274\217\346\200\273\347\273\223.md" "b/Part1/Android/Android\345\206\205\345\255\230\346\263\204\346\274\217\346\200\273\347\273\223.md" index aebd000..445f397 100644 --- "a/Part1/Android/Android\345\206\205\345\255\230\346\263\204\346\274\217\346\200\273\347\273\223.md" +++ "b/Part1/Android/Android\345\206\205\345\255\230\346\263\204\346\274\217\346\200\273\347\273\223.md" @@ -11,9 +11,9 @@ Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式 * 静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。 -* 栈区 :当方法被执行时,方法体内的局部变量都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 +* 栈区 :当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 -* 堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。 +* 堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。 ##栈与堆的区别: @@ -24,7 +24,7 @@ Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式 举个例子: ``` -public class Sample() { +public class Sample { int s1 = 0; Sample mSample1 = new Sample(); @@ -97,7 +97,7 @@ for (int i = 1; i < 100; i++) { 2.Java内存泄漏引起的原因 -内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示你Out of memory。 +内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示你Out of memory。j Java内存泄漏的根本原因是什么呢?长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。具体主要有如下几大类: diff --git "a/Part1/Android/Android\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/Part1/Android/Android\345\237\272\347\241\200\347\237\245\350\257\206.md" index 95f04a8..359b62f 100644 --- "a/Part1/Android/Android\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ "b/Part1/Android/Android\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -4,11 +4,11 @@ * FrameLayout(框架布局) - 此布局是五中布局中最简单的布局,Android中并没有对child view的摆布进行控制,这个布局中所有的控件都会默认出现在视图的左上角,我们可以使用``android:layout_margin``,``android:layout_gravity``等属性去控制子控件相对布局的位置。 + 此布局是五种布局中最简单的布局,Android中并没有对child view的摆布进行控制,这个布局中所有的控件都会默认出现在视图的左上角,我们可以使用``android:layout_margin``,``android:layout_gravity``等属性去控制子控件相对布局的位置。 * LinearLayout(线性布局) - 一行只控制一个控件的线性布局,所以当有很多控件需要在一个界面中列出时,可以用LinearLayout布局。 +    一行(或一列)只控制一个控件的线性布局,所以当有很多控件需要在一个界面中列出时,可以用LinearLayout布局。 此布局有一个需要格外注意的属性:``android:orientation=“horizontal|vertical``。 * 当`android:orientation="horizontal`时,*说明你希望将水平方向的布局交给**LinearLayout** *,其子元素的`android:layout_gravity="right|left"` 等控制水平方向的gravity值都是被忽略的,*此时**LinearLayout**中的子元素都是默认的按照水平从左向右来排*,我们可以用`android:layout_gravity="top|bottom"`等gravity值来控制垂直展示。 @@ -20,7 +20,7 @@ * RelativeLayout(相对布局) - 这个布局也是相对自由的布局,Android 对该布局的child view的 水平layout& 垂直layout做了解析,由此我们可以FrameLayout的基础上使用标签或者Java代码对垂直方向 以及 水平方向 布局中的views任意的控制. + 这个布局也是相对自由的布局,Android 对该布局的child view的 水平layout& 垂直layout做了解析,由此我们可以FrameLayout的基础上使用标签或者Java代码对垂直方向 以及 水平方向 布局中的views进行任意的控制. * 相关属性: @@ -51,8 +51,8 @@ onRestart()—>**onStart()**—>onResume(),再次回到运行状态。 * Activity退居后台,且系统内存不足, 系统会杀死这个后台状态的Activity(此时这个Activity引用仍然处在任务栈中,只是这个时候引用指向的对象已经为null),若再次回到这个Activity,则会走onCreate()–>onStart()—>onResume()(将重新走一次Activity的初始化生命周期) -* 锁定屏与解锁屏幕 - **只会调用onPause(),而不会调用onStop()方法,开屏后则调用onResume()** +* 锁屏:`onPause()->onStop()` +* 解锁:`onStart()->onResume()` * 更多流程分支,请参照以下生命周期流程图 ![](http://img.blog.csdn.net/20130828141902812) @@ -68,7 +68,7 @@ **任务栈**是一种后进先出的结构。位于栈顶的Activity处于焦点状态,当按下back按钮的时候,栈内的Activity会一个一个的出栈,并且调用其``onDestory()``方法。如果栈内没有Activity,那么系统就会回收这个栈,每个APP默认只有一个栈,以APP的包名来命名. * standard : 标准模式,每次启动Activity都会创建一个新的Activity实例,并且将其压入任务栈栈顶,而不管这个Activity是否已经存在。Activity的启动三回调(*onCreate()->onStart()->onResume()*)都会执行。 - - singleTop : 栈顶复用模式.这种模式下,如果新Activity已经位于任务栈的栈顶,那么此Activity不会被重新创建,所以它的启动三回调就不会执行,同时Activity的``onNewIntent()``方法会被回调.如果Activity已经存在但是不在栈顶,那么作用于*standard模式*一样. + - singleTop : 栈顶复用模式.这种模式下,如果新Activity已经位于任务栈的栈顶,那么此Activity不会被重新创建,所以它的启动三回调就不会执行,同时Activity的``onNewIntent()``方法会被回调.如果Activity已经存在但是不在栈顶,那么作用与*standard模式*一样. - singleTask: 栈内复用模式.创建这样的Activity的时候,系统会先确认它所需任务栈已经创建,否则先创建任务栈.然后放入Activity,如果栈中已经有一个Activity实例,那么这个Activity就会被调到栈顶,``onNewIntent()``,并且singleTask会清理在当前Activity上面的所有Activity.(clear top) - singleInstance : 加强版的singleTask模式,这种模式的Activity只能单独位于一个任务栈内,由于栈内复用的特性,后续请求均不会创建新的Activity,除非这个独特的任务栈被系统销毁了 @@ -128,9 +128,9 @@ Activity的堆栈管理以ActivityRecord为单位,所有的ActivityRecord都放 总而言之,onSaveInstanceState的调用遵循一个重要原则,即当系统“未经你许可”时销毁了你的activity,则onSaveInstanceState会被系统调用,这是系统的责任,因为它必须要提供一个机会让你保存你的数据(当然你不保存那就随便你了)。另外,需要注意的几点: -1.布局中的每一个View默认实现了onSaveInstanceState()方法,这样的话,这个UI的任何改变都会自动的存储和在activity重新创建的时候自动的恢复。但是这种情况只有在你为这个UI提供了唯一的ID之后才起作用,如果没有提供ID,将不会存储它的状态。 +1.布局中的每一个View默认实现了onSaveInstanceState()方法,这样的话,这个UI的任何改变都会自动地存储和在activity重新创建的时候自动地恢复。但是这种情况只有在你为这个UI提供了唯一的ID之后才起作用,如果没有提供ID,app将不会存储它的状态。 -2.由于默认的onSaveInstanceState()方法的实现帮助UI存储它的状态,所以如果你需要覆盖这个方法去存储额外的状态信息时,你应该在执行任何代码之前都调用父类的onSaveInstanceState()方法(super.onSaveInstanceState())。 +2.由于默认的onSaveInstanceState()方法的实现帮助UI存储它的状态,所以如果你需要覆盖这个方法去存储额外的状态信息,你应该在执行任何代码之前都调用父类的onSaveInstanceState()方法(super.onSaveInstanceState())。 既然有现成的可用,那么我们到底还要不要自己实现onSaveInstanceState()?这得看情况了,如果你自己的派生类中有变量影响到UI,或你程序的行为,当然就要把这个变量也保存了,那么就需要自己实现,否则就不需要。 3.由于onSaveInstanceState()方法调用的不确定性,你应该只使用这个方法去记录activity的瞬间状态(UI的状态)。不应该用这个方法去存储持久化数据。当用户离开这个activity的时候应该在onPause()方法中存储持久化数据(例如应该被存储到数据库中的数据)。 @@ -217,7 +217,7 @@ public void onRestoreInstanceState(Bundle savedInstanceState) { android:enabled="true" /> ``` -**广播(Boardcast Receiver)的两种动态注册和静态注册有什么区别。** +**广播(Broadcast Receiver)的两种动态注册和静态注册有什么区别。** * 静态注册:在AndroidManifest.xml文件中进行注册,当App退出后,Receiver仍然可以接收到广播并且进行相应的处理 * 动态注册:在代码中动态注册,当App退出后,也就没办法再接受广播了 @@ -247,7 +247,7 @@ public void onRestoreInstanceState(Bundle savedInstanceState) { * 【结论】如果在极度极度低内存的压力下,该service还是会被kill掉,并且不一定会restart() **onDestroy方法里重启service** - * service +broadcast 方式,就是当service走ondestory()的时候,发送一个自定义的广播,当收到广播的时候,重新启动service + * service +broadcast 方式,就是当service走onDestory()的时候,发送一个自定义的广播,当收到广播的时候,重新启动service * 也可以直接在onDestroy()里startService * 【结论】当使用类似口口管家等第三方应用或是在setting里-应用-强制停止时,APP进程可能就直接被干掉了,onDestroy方法都进不来,所以还是无法保证 @@ -306,7 +306,7 @@ Android为此数据库提供了一个名为SQLiteDatabase的类,封装了一 **如何判断应用被强杀** -在Applicatio中定义一个static常量,赋值为-1,在欢迎界面改为0,如果被强杀,application重新初始化,在父类Activity判断该常量的值。 +在Application中定义一个static常量,赋值为-1,在欢迎界面改为0,如果被强杀,application重新初始化,在父类Activity判断该常量的值。 **应用被强杀如何解决** @@ -317,8 +317,12 @@ Android为此数据库提供了一个名为SQLiteDatabase的类,封装了一 **怎样退出终止App** **Asset目录与res目录的区别。** +res 目录下面有很多文件,例如 drawable,mipmap,raw 等。res 下面除了 raw 文件不会被压缩外,其余文件都会被压缩。同时 res目录下的文件可以通过R 文件访问。Asset 也是用来存储资源,但是 asset 文件内容只能通过路径或者 AssetManager 读取。 [官方文档](https://developer.android.com/studio/projects/index.html) **Android怎么加速启动Activity。** +分两种情况,启动应用 和 普通Activity +启动应用 :Application 的构造方法,onCreate 方法中不要进行耗时操作,数据预读取(例如 init 数据) 放在异步中操作 +启动普通的Activity:A 启动B 时不要在 A 的 onPause 中执行耗时操作。因为 B 的 onResume 方法必须等待 A 的 onPause 执行完成后才能运行 **Android内存优化方法:ListView优化,及时关闭资源,图片缓存等等。** @@ -432,7 +436,7 @@ onStartCommand中回调了onStart,onStart中通过mServiceHandler发送消息 构建工具、Groovy语法、Java -Jar包里面只有代码,aar里面不光有代码还包括 +Jar包里面只有代码,aar里面不光有代码还包括代码还包括资源文件,比如 drawable 文件,xml 资源文件。对于一些不常变动的 Android Library,我们可以直接引用 aar,加快编译速度 --- diff --git "a/Part1/Android/Bitmap\347\232\204\345\210\206\346\236\220\344\270\216\344\275\277\347\224\250.md" "b/Part1/Android/Bitmap\347\232\204\345\210\206\346\236\220\344\270\216\344\275\277\347\224\250.md" new file mode 100644 index 0000000..32344ed --- /dev/null +++ "b/Part1/Android/Bitmap\347\232\204\345\210\206\346\236\220\344\270\216\344\275\277\347\224\250.md" @@ -0,0 +1,204 @@ +## **Bitmap**的分析与使用 + - Bitmap的创建 + - 创建Bitmap的时候,Java不提供`new Bitmap()`的形式去创建,而是通过`BitmapFactory`中的静态方法去创建,如:`BitmapFactory.decodeStream(is);//通过InputStream去解析生成Bitmap`(这里就不贴`BitmapFactory`中创建`Bitmap`的方法了,大家可以自己去看它的源码),我们跟进`BitmapFactory`中创建`Bitmap`的源码,最终都可以追溯到这几个native函数 + ``` + private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage, + Rect padding, Options opts); + private static native Bitmap nativeDecodeFileDescriptor(FileDescriptor fd, + Rect padding, Options opts); + private static native Bitmap nativeDecodeAsset(long nativeAsset, Rect padding, Options opts); + private static native Bitmap nativeDecodeByteArray(byte[] data, int offset, + int length, Options opts); + ``` + 而`Bitmap`又是Java对象,这个Java对象又是从native,也就是C/C++中产生的,所以,在Android中Bitmap的内存管理涉及到两部分,一部分是*native*,另一部分是*dalvik*,也就是我们常说的java堆(如果对java堆与栈不了解的同学可以戳),到这里基本就已经了解了创建Bitmap的一些内存中的特性(大家可以使用``adb shell dumpsys meminfo``去查看Bitmap实例化之后的内存使用情况)。 + + - Bitmap的使用 + - 我们已经知道了`BitmapFactory`是如何通过各种资源创建`Bitmap`了,那么我们如何合理的使用它呢?以下是几个我们使用`Bitmap`需要关注的点 + 1. **Size** + - 这里我们来算一下,在Android中,如果采用`Config.ARGB_8888`的参数去创建一个`Bitmap`,[这是Google推荐的配置色彩参数](https://developer.android.com/reference/android/graphics/Bitmap.Config.html),也是Android4.4及以上版本默认创建Bitmap的Config参数(``Bitmap.Config.inPreferredConfig``的默认值),那么每一个像素将会占用4byte,如果一张手机照片的尺寸为1280×720,那么我们可以很容易的计算出这张图片占用的内存大小为 1280x720x4 = 3686400(byte) = 3.5M,一张未经处理的照片就已经3.5M了! 显而易见,在开发当中,这是我们最需要关注的问题,否则分分钟OOM! + - *那么,我们一般是如何处理Size这个重要的因素的呢?*,当然是调整`Bitmap`的大小到适合的程度啦!辛亏在`BitmapFactory`中,我们可以很方便的通过`BitmapFactory.Options`中的`options.inSampleSize`去设置`Bitmap`的压缩比,官方给出的说法是 + > If set to a value > 1, requests the decoder to subsample the original image, returning a smaller image to save memory....For example, inSampleSize == 4 returns + an image that is 1/4 the width/height of the original, and 1/16 the + number of pixels. Any value <= 1 is treated the same as 1. + + 很简洁明了啊!也就是说,只要按计算方法设置了这个参数,就可以完成我们Bitmap的Size调整了。那么,应该怎么调整姿势才比较舒服呢?下面先介绍其中一种通过``InputStream``的方式去创建``Bitmap``的方法,上一段从Gallery中获取照片并且将图片Size调整到合适手机尺寸的代码: + ``` + static final int PICK_PICS = 9; + + public void startGallery(){ + Intent i = new Intent(); + i.setAction(Intent.ACTION_PICK); + i.setType("image/*"); + startActivityForResult(i,PICK_PICS); + } + + private int[] getScreenWithAndHeight(){ + WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics dm = new DisplayMetrics(); + wm.getDefaultDisplay().getMetrics(dm); + return new int[]{dm.widthPixels,dm.heightPixels}; + } + + /** + * + * @param actualWidth 图片实际的宽度,也就是options.outWidth + * @param actualHeight 图片实际的高度,也就是options.outHeight + * @param desiredWidth 你希望图片压缩成为的目的宽度 + * @param desiredHeight 你希望图片压缩成为的目的高度 + * @return + */ + private int findBestSampleSize(int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) { + double wr = (double) actualWidth / desiredWidth; + double hr = (double) actualHeight / desiredHeight; + double ratio = Math.min(wr, hr); + float n = 1.0f; + //这里我们为什么要寻找 与ratio最接近的2的倍数呢? + //原因就在于API中对于inSimpleSize的注释:最终的inSimpleSize应该为2的倍数,我们应该向上取与压缩比最接近的2的倍数。 + while ((n * 2) <= ratio) { + n *= 2; + } + + return (int) n; + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if(resultCode == RESULT_OK){ + switch (requestCode){ + case PICK_PICS: + Uri uri = data.getData(); + InputStream is = null; + try { + is = getContentResolver().openInputStream(uri); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + + BitmapFactory.Options options = new BitmapFactory.Options(); + //当这个参数为true的时候,意味着你可以在解析时候不申请内存的情况下去获取Bitmap的宽和高 + //这是调整Bitmap Size一个很重要的参数设置 + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream( is,null,options ); + + int realHeight = options.outHeight; + int realWidth = options.outWidth; + + int screenWidth = getScreenWithAndHeight()[0]; + + int simpleSize = findBestSampleSize(realWidth,realHeight,screenWidth,300); + options.inSampleSize = simpleSize; + //当你希望得到Bitmap实例的时候,不要忘了将这个参数设置为false + options.inJustDecodeBounds = false; + + try { + is = getContentResolver().openInputStream(uri); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + + Bitmap bitmap = BitmapFactory.decodeStream(is,null,options); + + iv.setImageBitmap(bitmap); + + try { + is.close(); + is = null; + } catch (IOException e) { + e.printStackTrace(); + } + + break; + } + } + super.onActivityResult(requestCode, resultCode, data); + } + ``` + + 我们来看看这段代码的功效: + 压缩前:![压缩前](https://leanote.com/api/file/getImage?fileId=578d9ed8ab644135ea01684c) + 压缩后:![压缩后](https://leanote.com/api/file/getImage?fileId=578d9f76ab644135ea016851) + **对比条件为:1080P的魅族Note3拍摄的高清无码照片** + + 2. **Reuse** + 上面介绍了``BitmapFactory``通过``InputStream``去创建`Bitmap`的这种方式,以及``BitmapFactory.Options.inSimpleSize`` 和 ``BitmapFactory.Options.inJustDecodeBounds``的使用方法,但将单个Bitmap加载到UI是简单的,但是如果我们需要一次性加载大量的图片,事情就会变得复杂起来。`Bitmap`是吃内存大户,我们不希望多次解析相同的`Bitmap`,也不希望可能不会用到的`Bitmap`一直存在于内存中,所以,这个场景下,`Bitmap`的重用变得异常的重要。 + *在这里只介绍一种``BitmapFactory.Options.inBitmap``的重用方式,下一篇文章会介绍使用三级缓存来实现Bitmap的重用。* + + 根据官方文档[在Android 3.0 引进了BitmapFactory.Options.inBitmap](https://developer.android.com/reference/android/graphics/BitmapFactory.Options.html#inBitmap),如果这个值被设置了,decode方法会在加载内容的时候去重用已经存在的bitmap. 这意味着bitmap的内存是被重新利用的,这样可以提升性能, 并且减少了内存的分配与回收。然而,使用inBitmap有一些限制。特别是在Android 4.4 之前,只支持同等大小的位图。 + 我们看来看看这个参数最基本的运用方法。 + + ``` + new BitmapFactory.Options options = new BitmapFactory.Options(); + //inBitmap只有当inMutable为true的时候是可用的。 + options.inMutable = true; + Bitmap reusedBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.reused_btimap,options); + options.inBitmap = reusedBitmap; + ``` + + 这样,当你在下一次decodeBitmap的时候,将设置了`options.inMutable=true`以及`options.inBitmap`的`Options`传入,Android就会复用你的Bitmap了,具体实例: + + ``` + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(reuseBitmap()); + } + + private LinearLayout reuseBitmap(){ + LinearLayout linearLayout = new LinearLayout(this); + linearLayout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + linearLayout.setOrientation(LinearLayout.VERTICAL); + + ImageView iv = new ImageView(this); + iv.setLayoutParams(new ViewGroup.LayoutParams(500,300)); + + options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + //inBitmap只有当inMutable为true的时候是可用的。 + options.inMutable = true; + BitmapFactory.decodeResource(getResources(),R.drawable.big_pic,options); + + //压缩Bitmap到我们希望的尺寸 + //确保不会OOM + options.inSampleSize = findBestSampleSize(options.outWidth,options.outHeight,500,300); + options.inJustDecodeBounds = false; + + Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.big_pic,options); + options.inBitmap = bitmap; + + iv.setImageBitmap(bitmap); + + linearLayout.addView(iv); + + ImageView iv1 = new ImageView(this); + iv1.setLayoutParams(new ViewGroup.LayoutParams(500,300)); + iv1.setImageBitmap( BitmapFactory.decodeResource(getResources(),R.drawable.big_pic,options)); + linearLayout.addView(iv1); + + ImageView iv2 = new ImageView(this); + iv2.setLayoutParams(new ViewGroup.LayoutParams(500,300)); + iv2.setImageBitmap( BitmapFactory.decodeResource(getResources(),R.drawable.big_pic,options)); + linearLayout.addView(iv2); + + + return linearLayout; + } + ``` + + 以上代码中,我们在解析了一次一张1080P分辨率的图片,并且设置在`options.inBitmap`中,然后分别decode了同一张图片,并且传入了相同的`options`。最终只占用一份第一次解析`Bitmap`的内存。 + + 3. **Recycle** + 一定要记得及时回收Bitmap,否则如上分析,你的native以及dalvik的内存都会被一直占用着,最终导致OOM + + + ``` + // 先判断是否已经回收 + if(bitmap != null && !bitmap.isRecycled()){ + // 回收并且置为null + bitmap.recycle(); + bitmap = null; + } + System.gc(); + ``` + + - Enjoy Android :) 如果有误,轻喷,欢迎指正。 + diff --git "a/Part1/Android/EventBus\347\224\250\346\263\225\350\257\246\350\247\243.md" "b/Part1/Android/EventBus\347\224\250\346\263\225\350\257\246\350\247\243.md" index 3c26b70..28c7ce0 100644 --- "a/Part1/Android/EventBus\347\224\250\346\263\225\350\257\246\350\247\243.md" +++ "b/Part1/Android/EventBus\347\224\250\346\263\225\350\257\246\350\247\243.md" @@ -63,7 +63,9 @@ eventBus.post(event) 接收消息并处理: ``` -public void onEvent(MessageEvent event) {} +// 3.0后不再要求事件以 onEvent 开头,而是采用注解的方式 +@Subscribe(threadMode = ThreadMode.MAIN) +public void receive(MessageEvent event){} ``` 注销事件接收: @@ -72,6 +74,13 @@ public void onEvent(MessageEvent event) {} eventBus.unregister(this); ``` +索引加速: + +``` +3.0 后引入了索引加速(默认不开启)的功能,即通过 apt 编译插件的方式,在代码编译的时候对注解进行索引,避免了以往通过反射造成的性能损耗。 +如何使用可以参考[官方文档](http://greenrobot.org/eventbus/documentation/subscriber-index/) +``` + 最后,proguard 需要做一些额外处理: ``` diff --git "a/Part1/Android/Listview\350\257\246\350\247\243.md" "b/Part1/Android/Listview\350\257\246\350\247\243.md" index 1b943b8..ee533a7 100644 --- "a/Part1/Android/Listview\350\257\246\350\247\243.md" +++ "b/Part1/Android/Listview\350\257\246\350\247\243.md" @@ -1,6 +1,6 @@ #ListView详解 --- -直接继承自AbsListView,AbsListView继承自AdapterView,AdapterView继承自AdapterView,AdapterView又继承自ViewGroup。 +直接继承自AbsListView,AbsListView继承自AdapterView,AdapterView又继承自ViewGroup。 Adpater在ListView和数据源之间起到了一个桥梁的作用 @@ -15,4 +15,4 @@ RecycleBin机制是ListView能够实现成百上千条数据都不会OOM最重 * getScrapView 用于从废弃缓存中取出一个View,这些废弃缓存中的View是没有顺序可言的,因此getScrapView()方法中的算法也非常简单,就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回。 * 我们都知道Adapter当中可以重写一个getViewTypeCount()来表示ListView中有几种类型的数据项,而setViewTypeCount()方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制。 -View的流程分三步,onMeasure()用于测量View的大小,onLayout()用于确定View的布局,onDraw()用于将View绘制到界面上。 \ No newline at end of file +View的流程分三步,onMeasure()用于测量View的大小,onLayout()用于确定View的布局,onDraw()用于将View绘制到界面上。 diff --git "a/Part1/Android/\347\272\277\347\250\213\351\200\232\344\277\241\345\237\272\347\241\200\346\265\201\347\250\213\345\210\206\346\236\220.md" "b/Part1/Android/\347\272\277\347\250\213\351\200\232\344\277\241\345\237\272\347\241\200\346\265\201\347\250\213\345\210\206\346\236\220.md" new file mode 100644 index 0000000..defe0ca --- /dev/null +++ "b/Part1/Android/\347\272\277\347\250\213\351\200\232\344\277\241\345\237\272\347\241\200\346\265\201\347\250\213\345\210\206\346\236\220.md" @@ -0,0 +1,205 @@ +> 老司机们都知道,Android的线程间通信就靠Handler、Looper、Message、MessageQueue这四个麻瓜兄弟了,那么,他们是怎么运作的呢?下面做一个基于主要源代码的大学生水平的分析。 [原文链接](http://anangryant.leanote.com/post/Handler%E3%80%81Looper%E3%80%81Message%E3%80%81MessageQueue%E5%88%86%E6%9E%90) + +##Looper(先分析这个是因为能够引出四者的关系) +在Looper中,维持一个`Thread`对象以及`MessageQueue`,通过Looper的构造函数我们可以知道: +``` + private Looper(boolean quitAllowed) { + mQueue = new MessageQueue(quitAllowed);//传入的参数代表这个Queue是否能够被退出 + mThread = Thread.currentThread(); + } +``` +`Looper`在构造函数里干了两件事情: +1. 将线程对象指向了创建`Looper`的线程 +2. 创建了一个新的`MessageQueue` + +分析完构造函数之后,接下来我们主要分析两个方法: +1. `looper.loop()` +2. `looper.prepare()` + +###looper.loop()(在当前线程启动一个Message loop机制,此段代码将直接分析出Looper、Handler、Message、MessageQueue的关系) +``` + public static void loop() { + final Looper me = myLooper();//获得当前线程绑定的Looper + if (me == null) { + throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); + } + final MessageQueue queue = me.mQueue;//获得与Looper绑定的MessageQueue + + // Make sure the identity of this thread is that of the local process, + // and keep track of what that identity token actually is. + Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); + + //进入死循环,不断地去取对象,分发对象到Handler中消费 + for (;;) { + Message msg = queue.next(); // 不断的取下一个Message对象,在这里可能会造成堵塞。 + if (msg == null) { + // No message indicates that the message queue is quitting. + return; + } + + // This must be in a local variable, in case a UI event sets the logger + Printer logging = me.mLogging; + if (logging != null) { + logging.println(">>>>> Dispatching to " + msg.target + " " + + msg.callback + ": " + msg.what); + } + + //在这里,开始分发Message了 + //至于这个target是神马?什么时候被赋值的? + //我们一会分析Handler的时候就会讲到 + msg.target.dispatchMessage(msg); + + if (logging != null) { + logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); + } + + // Make sure that during the course of dispatching the + // identity of the thread wasn't corrupted. + final long newIdent = Binder.clearCallingIdentity(); + if (ident != newIdent) { + Log.wtf(TAG, "Thread identity changed from 0x" + + Long.toHexString(ident) + " to 0x" + + Long.toHexString(newIdent) + " while dispatching to " + + msg.target.getClass().getName() + " " + + msg.callback + " what=" + msg.what); + } + + //当分发完Message之后,当然要标记将该Message标记为 *正在使用* 啦 + msg.recycleUnchecked(); + } + } +``` +*分析了上面的源代码,我们可以意识到,最重要的方法是:* +1. `queue.next()` +2. `msg.target.dispatchMessage(msg)` +3. `msg.recycleUnchecked()` + +其实Looper中最重要的部分都是由`Message`、`MessageQueue`组成的有木有!这段最重要的代码中涉及到了四个对象,他们与彼此的关系如下: +1. `MessageQueue`:装食物的容器 +2. `Message`:被装的食物 +3. `Handler`(msg.target实际上就是`Handler`):食物的消费者 +4. `Looper`:负责分发食物的人 + + +###looper.prepare()(在当前线程关联一个Looper对象) +``` + private static void prepare(boolean quitAllowed) { + if (sThreadLocal.get() != null) { + throw new RuntimeException("Only one Looper may be created per thread"); + } + //在当前线程绑定一个Looper + sThreadLocal.set(new Looper(quitAllowed)); + } +``` +以上代码只做了两件事情: +1. 判断当前线程有木有`Looper`,如果有则抛出异常(在这里我们就可以知道,Android规定一个线程只能够拥有一个与自己关联的`Looper`)。 +2. 如果没有的话,那么就设置一个新的`Looper`到当前线程。 + +-------------- +##Handler +由于我们使用Handler的通常性的第一步是: +``` + Handler handler = new Handler(){ + //你们有没有很好奇这个方法是在哪里被回调的? + //我也是!所以接下来会分析到哟! + @Override + public void handleMessage(Message msg) { + //Handler your Message + } + }; +``` +所以我们先来分析`Handler`的构造方法 +``` +//空参数的构造方法与之对应,这里只给出主要的代码,具体大家可以到源码中查看 +public Handler(Callback callback, boolean async) { + //打印内存泄露提醒log + .... + + //获取与创建Handler线程绑定的Looper + mLooper = Looper.myLooper(); + if (mLooper == null) { + throw new RuntimeException( + "Can't create handler inside thread that has not called Looper.prepare()"); + } + //获取与Looper绑定的MessageQueue + //因为一个Looper就只有一个MessageQueue,也就是与当前线程绑定的MessageQueue + mQueue = mLooper.mQueue; + mCallback = callback; + mAsynchronous = async; + + } +``` +*带上问题:* +1. `Looper.loop()`死循环中的`msg.target`是什么时候被赋值的? +2. `handler.handleMessage(msg)`在什么时候被回调的? + +###A1:`Looper.loop()`死循环中的`msg.target`是什么时候被赋值的? +要分析这个问题,很自然的我们想到从发送消息开始,无论是`handler.sendMessage(msg)`还是`handler.sendEmptyMessage(what)`,我们最终都可以追溯到以下方法 +``` +public boolean sendMessageAtTime(Message msg, long uptimeMillis) { + //引用Handler中的MessageQueue + //这个MessageQueue就是创建Looper时被创建的MessageQueue + MessageQueue queue = mQueue; + + if (queue == null) { + RuntimeException e = new RuntimeException( + this + " sendMessageAtTime() called with no mQueue"); + Log.w("Looper", e.getMessage(), e); + return false; + } + //将新来的Message加入到MessageQueue中 + return enqueueMessage(queue, msg, uptimeMillis); + } +``` + +我们接下来分析`enqueueMessage(queue, msg, uptimeMillis)`: +``` +private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { + //显而易见,大写加粗的赋值啊! + **msg.target = this;** + if (mAsynchronous) { + msg.setAsynchronous(true); + } + return queue.enqueueMessage(msg, uptimeMillis); + } +``` + + +###A2:`handler.handleMessage(msg)`在什么时候被回调的? +通过以上的分析,我们很明确的知道`Message`中的`target`是在什么时候被赋值的了,我们先来分析在`Looper.loop()`中出现过的过的`dispatchMessage(msg)`方法 + +``` +public void dispatchMessage(Message msg) { + if (msg.callback != null) { + handleCallback(msg); + } else { + if (mCallback != null) { + if (mCallback.handleMessage(msg)) { + return; + } + } + //看到这个大写加粗的方法调用没! + **handleMessage(msg);** + } + } +``` + +加上以上分析,我们将之前分析结果串起来,就可以知道了某些东西: +`Looper.loop()`不断地获取`MessageQueue`中的`Message`,然后调用与`Message`绑定的`Handler`对象的`dispatchMessage`方法,最后,我们看到了`handleMessage`就在`dispatchMessage`方法里被调用的。 + +------------------ +通过以上的分析,我们可以很清晰的知道Handler、Looper、Message、MessageQueue这四者的关系以及如何合作的了。 + +#总结: +当我们调用`handler.sendMessage(msg)`方法发送一个`Message`时,实际上这个`Message`是发送到**与当前线程绑定**的一个`MessageQueue`中,然后**与当前线程绑定**的`Looper`将会不断的从`MessageQueue`中取出新的`Message`,调用`msg.target.dispathMessage(msg)`方法将消息分发到与`Message`绑定的`handler.handleMessage()`方法中。 + +一个`Thread`对应多个`Handler` +一个`Thread`对应一个`Looper`和`MessageQueue`,`Handler`与`Thread`共享`Looper`和`MessageQueue`。 +`Message`只是消息的载体,将会被发送到**与线程绑定的唯一的**`MessageQueue`中,并且被**与线程绑定的唯一的**`Looper`分发,被与其自身绑定的`Handler`消费。 + +------ +- Enjoy Android :) 如果有误,轻喷,欢迎指正。 + + + diff --git "a/Part1/DesignPattern/\345\215\225\344\276\213\346\250\241\345\274\217.md" "b/Part1/DesignPattern/\345\215\225\344\276\213\346\250\241\345\274\217.md" index 9a925fd..23f759d 100644 --- "a/Part1/DesignPattern/\345\215\225\344\276\213\346\250\241\345\274\217.md" +++ "b/Part1/DesignPattern/\345\215\225\344\276\213\346\250\241\345\274\217.md" @@ -97,7 +97,7 @@ public class Singleton { /** * 静态初始化器,由JVM来保证线程安全 */ - private static Singleton instance = new Singleton(); + private static final Singleton instance = new Singleton(); } /** diff --git "a/Part2/JavaConcurrent/\347\224\237\344\272\247\350\200\205\345\222\214\346\266\210\350\264\271\350\200\205\351\227\256\351\242\230.md" "b/Part2/JavaConcurrent/\347\224\237\344\272\247\350\200\205\345\222\214\346\266\210\350\264\271\350\200\205\351\227\256\351\242\230.md" index 104bbe0..d3e0b1d 100755 --- "a/Part2/JavaConcurrent/\347\224\237\344\272\247\350\200\205\345\222\214\346\266\210\350\264\271\350\200\205\351\227\256\351\242\230.md" +++ "b/Part2/JavaConcurrent/\347\224\237\344\272\247\350\200\205\345\222\214\346\266\210\350\264\271\350\200\205\351\227\256\351\242\230.md" @@ -111,10 +111,10 @@ public class PublicResource { // TODO Auto-generated catch block e.printStackTrace(); } - number++; - System.out.println("生产了1个,总共有" + number); - notifyAll(); } + number++; + System.out.println("生产了1个,总共有" + number); + notifyAll(); } diff --git "a/Part2/JavaSE/HashTable\346\272\220\347\240\201\345\211\226\346\236\220.md" "b/Part2/JavaSE/HashTable\346\272\220\347\240\201\345\211\226\346\236\220.md" index 032cc20..99adfce 100644 --- "a/Part2/JavaSE/HashTable\346\272\220\347\240\201\345\211\226\346\236\220.md" +++ "b/Part2/JavaSE/HashTable\346\272\220\347\240\201\345\211\226\346\236\220.md" @@ -68,6 +68,24 @@ public class Hashtable // 将“子Map”的全部元素都添加到Hashtable中 putAll(t); } + + private int hash(Object k) { + if (useAltHashing) { + if (k.getClass() == String.class) { + return sun.misc.Hashing.stringHash32((String) k); + } else { + int h = hashSeed ^ k.hashCode(); + + // This function ensures that hashCodes that differ only by + // constant multiples at each bit position have a bounded + // number of collisions (approximately 8 at default load factor). + h ^= (h >>> 20) ^ (h >>> 12); + return h ^ (h >>> 7) ^ (h >>> 4); + } + } else { + return k.hashCode(); + } + } public synchronized int size() { return count; @@ -131,7 +149,7 @@ public class Hashtable // 返回key对应的value,没有的话返回null public synchronized V get(Object key) { Entry tab[] = table; - int hash = key.hashCode(); + int hash = hash(key); // 计算索引值, int index = (hash & 0x7FFFFFFF) % tab.length; // 找到“key对应的Entry(链表)”,然后在链表中找出“哈希值”和“键值”与key都相等的元素 @@ -179,7 +197,7 @@ public class Hashtable // 若“Hashtable中已存在键为key的键值对”, // 则用“新的value”替换“旧的value” Entry tab[] = table; - int hash = key.hashCode(); + int hash = hash(key); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { @@ -211,7 +229,7 @@ public class Hashtable // 删除Hashtable中键为key的元素 public synchronized V remove(Object key) { Entry tab[] = table; - int hash = key.hashCode(); + int hash = hash(key); int index = (hash & 0x7FFFFFFF) % tab.length; //从table[index]链表中找出要删除的节点,并删除该节点。 @@ -376,7 +394,7 @@ public class Hashtable Map.Entry entry = (Map.Entry)o; Object key = entry.getKey(); Entry[] tab = table; - int hash = key.hashCode(); + int hash = hash(key); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry e = tab[index]; e != null; e = e.next) @@ -394,7 +412,7 @@ public class Hashtable Map.Entry entry = (Map.Entry) o; K key = entry.getKey(); Entry[] tab = table; - int hash = key.hashCode(); + int hash = hash(key); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry e = tab[index], prev = null; e != null; @@ -837,7 +855,7 @@ public class Hashtable public synchronized boolean containsKey(Object key) { Entry tab[] = table; /计算hash值,直接用key的hashCode代替 - int hash = key.hashCode(); + int hash = hash(key); // 计算在数组中的索引值 int index = (hash & 0x7FFFFFFF) % tab.length; // 找到“key对应的Entry(链表)”,然后在链表中找出“哈希值”和“键值”与key都相等的元素 @@ -852,4 +870,4 @@ public class Hashtable 很明显,如果value为null,会直接抛出NullPointerException异常,但源码中并没有对key是否为null判断,有点小不解!不过NullPointerException属于RuntimeException异常,是可以由JVM自动抛出的,也许对key的值在JVM中有所限制吧。 4. Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。 -5. Hashtable计算hash值,直接用key的hashCode(),而HashMap重新计算了key的hash值,Hashtable在求hash值对应的位置索引时,用取模运算,而HashMap在求位置索引时,则用与运算,且这里一般先用hash&0x7FFFFFFF后,再对length取模,&0x7FFFFFFF的目的是为了将负的hash值转化为正值,因为hash值有可能为负数,而&0x7FFFFFFF后,只有符号外改变,而后面的位都不变。 \ No newline at end of file +5. Hashtable和HashMap都重新计算了key的hash值,Hashtable在求hash值对应的位置索引时,用取模运算,而HashMap在求位置索引时,则用与运算,且这里一般先用hash&0x7FFFFFFF后,再对length取模,&0x7FFFFFFF的目的是为了将负的hash值转化为正值,因为hash值有可能为负数,而&0x7FFFFFFF后,只有符号外改变,而后面的位都不变。 \ No newline at end of file diff --git "a/Part2/JavaSE/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/Part2/JavaSE/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" index 46ab7fa..782ade9 100644 --- "a/Part2/JavaSE/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ "b/Part2/JavaSE/Java\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -20,10 +20,74 @@ **equals与==的区别。** [http://www.importnew.com/6804.html](http://www.importnew.com/6804.html) +> ==与equals的主要区别是:==常用于比较原生类型,而equals()方法用于检查对象的相等性。另一个不同的点是:如果==和equals()用于比较对象,当两个引用地址相同,==返回true。而equals()可以返回true或者false主要取决于重写实现。最常见的一个例子,字符串的比较,不同情况==和equals()返回不同的结果。equals()方法最重要的一点是,能够根据业务要求去重写,按照自定义规则去判断两个对象是否相等。重写equals()方法的时候,要注意一下hashCode是否会因为对象的属性改变而改变,否则在使用散列集合储存该对象的时候会碰到坑!!理解equals()方法的存在是很重要的。 + +1. 使用==比较有两种情况: + + 比较基础数据类型(Java中基础数据类型包括八中:short,int,long,float,double,char,byte,boolen):这种情况下,==比较的是他们的值是否相等。 + 引用间的比较:在这种情况下,==比较的是他们在内存中的地址,也就是说,除非引用指向的是同一个new出来的对象,此时他们使用`==`去比较得到true,否则,得到false。 +2. 使用equals进行比较: + + equals追根溯源,是Object类中的一个方法,在该类中,equals的实现也仅仅只是比较两个对象的内存地址是否相等,但在一些子类中,如:String、Integer 等,该方法将被重写。 + +3. 以`String`类为例子说明`eqauls`与`==`的区别: +> 在开始这个例子之前,同学们需要知道JVM处理String的一些特性。*Java的虚拟机在内存中开辟出一块单独的区域,用来存储字符串对象,这块内存区域被称为字符串缓冲池。*当使用 +`String a = "abc"`这样的语句进行定义一个引用的时候,首先会在*字符串缓冲池*中查找是否已经相同的对象,如果存在,那么就直接将这个对象的引用返回给a,如果不存在,则需要新建一个值为"abc"的对象,再将新的引用返回a。`String a = new String("abc");`这样的语句明确告诉JVM想要产生一个新的String对象,并且值为"abc",于是就*在堆内存中的某一个小角落开辟了一个新的String对象*。 + + - `==`在比较引用的情况下,会去比较两个引用的内存地址是否相等。 + ``` + String str1 = "abc"; + String str2 = "abc"; + + System.out.println(str1 == str2); + System.out.println(str1.equals(str2)); + + String str2 = new String("abc"); + System.out.println(str1 == str2); + System.out.println(str1.equals(str2)); + + ``` + 以上代码将会输出 + true + true + false + true + **第一个true:**因为在str2赋值之前,str1的赋值操作就已经在内存中创建了一个值为"abc"的对象了,然后str2将会与str1指向相同的地址。 + **第二个true:**因为`String`已经重写了`equals`方法:为了方便大家阅读我贴出来,并且在注释用进行分析: + ``` + public boolean equals(Object anObject) { + //如果比较的对象与自身内存地址相等的话 + //就说明他两指向的是同一个对象 + //所以此时equals的返回值跟==的结果是一样的。 + if (this == anObject) { + return true; + } + //当比较的对象与自身的内存地址不相等,并且 + //比较的对象是String类型的时候 + //将会执行这个分支 + if (anObject instanceof String) { + String anotherString = (String)anObject; + int n = value.length; + if (n == anotherString.value.length) { + char v1[] = value; + char v2[] = anotherString.value; + int i = 0; + //在这里循环遍历两个String中的char + while (n-- != 0) { + //只要有一个不相等,那么就会返回false + if (v1[i] != v2[i]) + return false; + i++; + } + return true; + } + } + return false; + } + ``` + 进行以上分析之后,就不难理解第一段代码中的实例程序输出了。 -主要区别在于前者是方法后者是操作符。“==”的行为对于每个对象来说与equals()是完全相同的,但是equals()可以基于业务规则的不同而重写(overridden )。“==”习惯用于原生(primitive)类型之间的比较,而equals()仅用于对象之间的比较。 -==与equals的主要区别是:==常用于比较原生类型,而equals()方法用于检查对象的相等性。另一个不同的点是:如果==和equals()用于比较对象,当两个引用地址相同,==返回true。而equals()可以返回true或者false主要取决于重写实现。最常见的一个例子,字符串的比较,不同情况==和equals()返回不同的结果。 --- @@ -57,9 +121,9 @@ final方法,获得运行时类型。 该方法用于哈希查找,可以减少在查找中使用equals的次数,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。 -一般必须满足obj1.equals(obj2)==true。可以推出obj1.hash- Code()==obj2.hashCode(),但是hashCode相等不一定就满足equals。不过为了提高效率,应该尽量使上面两个条件接近等价。 +一般必须满足obj1.equals(obj2)==true。可以推出obj1.hashCode()==obj2.hashCode(),但是hashCode相等不一定就满足equals。不过为了提高效率,应该尽量使上面两个条件接近等价。 -如果不重写hashcode(),在HashSet中添加两个equals的对象,会将两个对象都加入进去。 +如果不重写hashCode(),在HashSet中添加两个equals的对象,会将两个对象都加入进去。 7.wait方法 @@ -131,7 +195,7 @@ Java 平台提供了两种类型的字符串:String和StringBuffer / StringBui **try catch finally,try里有return,finally还执行么?** -会执行,在方法 返回调用者前执行。Java允许在finally中改变返回值的做法是不好的,因为如果存在finally代码块,try中的return语句不会立马返回调用者,而是纪录下返回值待finally代码块执行完毕之后再向调用者返回其值,然后如果在finally中修改了返回值,这会对程序造成很大的困扰,C#中国就从语法规定不能做这样的事。 +会执行,在方法 返回调用者前执行。Java允许在finally中改变返回值的做法是不好的,因为如果存在finally代码块,try中的return语句不会立马返回调用者,而是纪录下返回值待finally代码块执行完毕之后再向调用者返回其值,然后如果在finally中修改了返回值,这会对程序造成很大的困扰,C#中就从语法规定不能做这样的事。 --- diff --git "a/Part3/Algorithm/Sort/\351\235\242\350\257\225\344\270\255\347\232\204 10 \345\244\247\346\216\222\345\272\217\347\256\227\346\263\225\346\200\273\347\273\223.md" "b/Part3/Algorithm/Sort/\351\235\242\350\257\225\344\270\255\347\232\204 10 \345\244\247\346\216\222\345\272\217\347\256\227\346\263\225\346\200\273\347\273\223.md" new file mode 100644 index 0000000..626f256 --- /dev/null +++ "b/Part3/Algorithm/Sort/\351\235\242\350\257\225\344\270\255\347\232\204 10 \345\244\247\346\216\222\345\272\217\347\256\227\346\263\225\346\200\273\347\273\223.md" @@ -0,0 +1,647 @@ +

本文转载自码农网:http://www.codeceo.com/article/10-sort-algorithm-interview.html#0-tsina-1-10490-397232819ff9a47a7b7e80a40613cfe1 +

+ + +

查找和排序算法是算法的入门知识,其经典思想可以用于很多算法当中。因为其实现代码较短,应用较常见。所以在面试中经常会问到排序算法及其相关的问题。但万变不离其宗,只要熟悉了思想,灵活运用也不是难事。一般在面试中最常考的是快速排序和归并排序,并且经常有面试官要求现场写出这两种排序的代码。对这两种排序的代码一定要信手拈来才行。还有插入排序、冒泡排序、堆排序、基数排序、桶排序等。面试官对于这些排序可能会要求比较各自的优劣、各种算法的思想及其使用场景。还有要会分析算法的时间和空间复杂度。通常查找和排序算法的考察是面试的开始,如果这些问题回答不好,估计面试官都没有继续面试下去的兴趣都没了。所以想开个好头就要把常见的排序算法思想及其特点要熟练掌握,有必要时要熟练写出代码。

+

接下来我们就分析一下常见的排序算法及其使用场景。限于篇幅,某些算法的详细演示和图示请自行寻找详细的参考。

+

冒泡排序

+

冒泡排序是最简单的排序之一了,其大体思想就是通过与相邻元素的比较和交换来把小的数交换到最前面。这个过程类似于水泡向上升一样,因此而得名。举个栗子,对5,3,8,6,4这个无序序列进行冒泡排序。首先从后向前冒泡,4和6比较,把4交换到前面,序列变成5,3,8,4,6。同理4和8交换,变成5,3,4,8,6,3和4无需交换。5和3交换,变成3,5,4,8,6,3.这样一次冒泡就完了,把最小的数3排到最前面了。对剩下的序列依次冒泡就会得到一个有序序列。冒泡排序的时间复杂度为O(n^2)。

+

实现代码:

+
+
/**
+ *@Description:<p>冒泡排序算法实现</p>
+ *@author 王旭
+ *@time 2016-3-3 下午8:54:27
+ */
+public class BubbleSort {
+
+    public static void bubbleSort(int[] arr) {
+        if(arr == null || arr.length == 0)
+            return ;
+        for(int i=0; i<arr.length-1; i++) {
+            for(int j=arr.length-1; j>i; j--) {
+                if(arr[j] < arr[j-1]) {
+                    swap(arr, j-1, j);
+                }
+            }
+        }
+    }
+
+    public static void swap(int[] arr, int i, int j) {
+        int temp = arr[i];
+        arr[i] = arr[j];
+        arr[j] = temp;
+    }
+}
+
+

选择排序

+

选择排序的思想其实和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面。但是过程不同,冒泡排序是通过相邻的比较和交换。而选择排序是通过对整体的选择。举个栗子,对5,3,8,6,4这个无序序列进行简单选择排序,首先要选择5以外的最小数来和5交换,也就是选择3和5交换,一次排序后就变成了3,5,8,6,4.对剩下的序列一次进行选择和交换,最终就会得到一个有序序列。其实选择排序可以看成冒泡排序的优化,因为其目的相同,只是选择排序只有在确定了最小数的前提下才进行交换,大大减少了交换的次数。选择排序的时间复杂度为O(n^2)

+

实现代码:

+
+
/**
+ *@Description:<p>简单选择排序算法的实现</p>
+ *@author 王旭
+ *@time 2016-3-3 下午9:13:35
+ */
+public class SelectSort {
+
+    public static void selectSort(int[] arr) {
+        if(arr == null || arr.length == 0)
+            return ;
+        int minIndex = 0;
+        for(int i=0; i<arr.length-1; i++) { //只需要比较n-1次
+            minIndex = i;
+            for(int j=i+1; j<arr.length; j++) { //从i+1开始比较,因为minIndex默认为i了,i就没必要比了。
+                if(arr[j] < arr[minIndex]) {
+                    minIndex = j;
+                }
+            }
+
+            if(minIndex != i) { //如果minIndex不为i,说明找到了更小的值,交换之。
+                swap(arr, i, minIndex);
+            }
+        }
+
+    }
+
+    public static void swap(int[] arr, int i, int j) {
+        int temp = arr[i];
+        arr[i] = arr[j];
+        arr[j] = temp;
+    }
+
+}
+
+

插入排序

+

插入排序不是通过交换位置而是通过比较找到合适的位置插入元素来达到排序的目的的。相信大家都有过打扑克牌的经历,特别是牌数较大的。在分牌时可能要整理自己的牌,牌多的时候怎么整理呢?就是拿到一张牌,找到一个合适的位置插入。这个原理其实和插入排序是一样的。举个栗子,对5,3,8,6,4这个无序序列进行简单插入排序,首先假设第一个数的位置时正确的,想一下在拿到第一张牌的时候,没必要整理。然后3要插到5前面,把5后移一位,变成3,5,8,6,4.想一下整理牌的时候应该也是这样吧。然后8不用动,6插在8前面,8后移一位,4插在5前面,从5开始都向后移一位。注意在插入一个数的时候要保证这个数前面的数已经有序。简单插入排序的时间复杂度也是O(n^2)。

+

实现代码:

+
+
/**
+ *@Description:<p>简单插入排序算法实现</p>
+ *@author 王旭
+ *@time 2016-3-3 下午9:38:55
+ */
+public class InsertSort {
+
+    public static void insertSort(int[] arr) {
+        if(arr == null || arr.length == 0)
+            return ;
+
+        for(int i=1; i<arr.length; i++) { //假设第一个数位置时正确的;要往后移,必须要假设第一个。
+
+            int j = i;
+            int target = arr[i]; //待插入的
+
+            //后移
+            while(j > 0 && target < arr[j-1]) {
+                arr[j] = arr[j-1];
+                j --;
+            }
+
+            //插入 
+            arr[j] = target;
+        }
+
+    }
+
+}
+
+

快速排序

+

快速排序一听名字就觉得很高端,在实际应用当中快速排序确实也是表现最好的排序算法。冒泡排序虽然高端,但其实其思想是来自冒泡排序,冒泡排序是通过相邻元素的比较和交换把最小的冒泡到最顶端,而快速排序是比较和交换小数和大数,这样一来不仅把小数冒泡到上面同时也把大数沉到下面。

+

举个栗子:对5,3,8,6,4这个无序序列进行快速排序,思路是右指针找比基准数小的,左指针找比基准数大的,交换之。

+

5,3,8,6,4 用5作为比较的基准,最终会把5小的移动到5的左边,比5大的移动到5的右边。

+

5,3,8,6,4 首先设置i,j两个指针分别指向两端,j指针先扫描(思考一下为什么?)4比5小停止。然后i扫描,8比5大停止。交换i,j位置。

+

5,3,4,6,8 然后j指针再扫描,这时j扫描4时两指针相遇。停止。然后交换4和基准数。

+

4,3,5,6,8 一次划分后达到了左边比5小,右边比5大的目的。之后对左右子序列递归排序,最终得到有序序列。

+

上面留下来了一个问题为什么一定要j指针先动呢?首先这也不是绝对的,这取决于基准数的位置,因为在最后两个指针相遇的时候,要交换基准数到相遇的位置。一般选取第一个数作为基准数,那么就是在左边,所以最后相遇的数要和基准数交换,那么相遇的数一定要比基准数小。所以j指针先移动才能先找到比基准数小的数。

+

快速排序是不稳定的,其时间平均时间复杂度是O(nlgn)。

+

实现代码:

+
+
/**
+ *@Description:<p>实现快速排序算法</p>
+ *@author 王旭
+ *@time 2016-3-3 下午5:07:29
+ */
+public class QuickSort {
+    //一次划分
+    public static int partition(int[] arr, int left, int right) {
+        int pivotKey = arr[left];
+        int pivotPointer = left;
+
+        while(left < right) {
+            while(left < right && arr[right] >= pivotKey)
+                right --;
+            while(left < right && arr[left] <= pivotKey)
+                left ++;
+            swap(arr, left, right); //把大的交换到右边,把小的交换到左边。
+        }
+        swap(arr, pivotPointer, left); //最后把pivot交换到中间
+        return left;
+    }
+
+    public static void quickSort(int[] arr, int left, int right) {
+        if(left >= right)
+            return ;
+        int pivotPos = partition(arr, left, right);
+        quickSort(arr, left, pivotPos-1);
+        quickSort(arr, pivotPos+1, right);
+    }
+
+    public static void sort(int[] arr) {
+        if(arr == null || arr.length == 0)
+            return ;
+        quickSort(arr, 0, arr.length-1);
+    }
+
+    public static void swap(int[] arr, int left, int right) {
+        int temp = arr[left];
+        arr[left] = arr[right];
+        arr[right] = temp;
+    }
+
+}
+
+

其实上面的代码还可以再优化,上面代码中基准数已经在pivotKey中保存了,所以不需要每次交换都设置一个temp变量,在交换左右指针的时候只需要先后覆盖就可以了。这样既能减少空间的使用还能降低赋值运算的次数。优化代码如下:

+
+
/**
+ *@Description:<p>实现快速排序算法</p>
+ *@author 王旭
+ *@time 2016-3-3 下午5:07:29
+ */
+public class QuickSort {
+
+    /**
+     * 划分
+     * @param arr
+     * @param left
+     * @param right
+     * @return
+     */
+    public static int partition(int[] arr, int left, int right) {
+        int pivotKey = arr[left];
+
+        while(left < right) {
+            while(left < right && arr[right] >= pivotKey)
+                right --;
+            arr[left] = arr[right]; //把小的移动到左边
+            while(left < right && arr[left] <= pivotKey)
+                left ++;
+            arr[right] = arr[left]; //把大的移动到右边
+        }
+        arr[left] = pivotKey; //最后把pivot赋值到中间
+        return left;
+    }
+
+    /**
+     * 递归划分子序列
+     * @param arr
+     * @param left
+     * @param right
+     */
+    public static void quickSort(int[] arr, int left, int right) {
+        if(left >= right)
+            return ;
+        int pivotPos = partition(arr, left, right);
+        quickSort(arr, left, pivotPos-1);
+        quickSort(arr, pivotPos+1, right);
+    }
+
+    public static void sort(int[] arr) {
+        if(arr == null || arr.length == 0)
+            return ;
+        quickSort(arr, 0, arr.length-1);
+    }
+
+}
+
+

总结快速排序的思想:冒泡+二分+递归分治,慢慢体会。。。

+

堆排序

+

堆排序是借助堆来实现的选择排序,思想同简单的选择排序,以下以大顶堆为例。注意:如果想升序排序就使用大顶堆,反之使用小顶堆。原因是堆顶元素需要交换到序列尾部。

+

首先,实现堆排序需要解决两个问题:

+

1. 如何由一个无序序列键成一个堆?

+

2. 如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?

+

第一个问题,可以直接使用线性数组来表示一个堆,由初始的无序序列建成一个堆就需要自底向上从第一个非叶元素开始挨个调整成一个堆。

+

第二个问题,怎么调整成堆?首先是将堆顶元素和最后一个元素交换。然后比较当前堆顶元素的左右孩子节点,因为除了当前的堆顶元素,左右孩子堆均满足条件,这时需要选择当前堆顶元素与左右孩子节点的较大者(大顶堆)交换,直至叶子节点。我们称这个自堆顶自叶子的调整成为筛选。

+

从一个无序序列建堆的过程就是一个反复筛选的过程。若将此序列看成是一个完全二叉树,则最后一个非终端节点是n/2取底个元素,由此筛选即可。举个栗子:

+

49,38,65,97,76,13,27,49序列的堆排序建初始堆和调整的过程如下:

+

+

+

实现代码:

+
+
/**
+ *@Description:<p>堆排序算法的实现,以大顶堆为例。</p>
+ *@author 王旭
+ *@time 2016-3-4 上午9:26:02
+ */
+public class HeapSort {
+
+    /**
+     * 堆筛选,除了start之外,start~end均满足大顶堆的定义。
+     * 调整之后start~end称为一个大顶堆。
+     * @param arr 待调整数组
+     * @param start 起始指针
+     * @param end 结束指针
+     */
+    public static void heapAdjust(int[] arr, int start, int end) {
+        int temp = arr[start];
+
+        for(int i=2*start+1; i<=end; i*=2) {
+            //左右孩子的节点分别为2*i+1,2*i+2
+
+            //选择出左右孩子较小的下标
+            if(i < end && arr[i] < arr[i+1]) {
+                i ++; 
+            }
+            if(temp >= arr[i]) {
+                break; //已经为大顶堆,=保持稳定性。
+            }
+            arr[start] = arr[i]; //将子节点上移
+            start = i; //下一轮筛选
+        }
+
+        arr[start] = temp; //插入正确的位置
+    }
+
+    public static void heapSort(int[] arr) {
+        if(arr == null || arr.length == 0)
+            return ;
+
+        //建立大顶堆
+        for(int i=arr.length/2; i>=0; i--) {
+            heapAdjust(arr, i, arr.length-1);
+        }
+
+        for(int i=arr.length-1; i>=0; i--) {
+            swap(arr, 0, i);
+            heapAdjust(arr, 0, i-1);
+        }
+
+    }
+
+    public static void swap(int[] arr, int i, int j) {
+        int temp = arr[i];
+        arr[i] = arr[j];
+        arr[j] = temp;
+    }
+
+}
+
+

希尔排序

+

希尔排序是插入排序的一种高效率的实现,也叫缩小增量排序。简单的插入排序中,如果待排序列是正序时,时间复杂度是O(n),如果序列是基本有序的,使用直接插入排序效率就非常高。希尔排序就利用了这个特点。基本思想是:先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序。

+

举个栗子:

+

+

从上述排序过程可见,希尔排序的特点是,子序列的构成不是简单的逐段分割,而是将某个相隔某个增量的记录组成一个子序列。如上面的例子,第一堂排序时的增量为5,第二趟排序的增量为3。由于前两趟的插入排序中记录的关键字是和同一子序列中的前一个记录的关键字进行比较,因此关键字较小的记录就不是一步一步地向前挪动,而是跳跃式地往前移,从而使得进行最后一趟排序时,整个序列已经做到基本有序,只要作记录的少量比较和移动即可。因此希尔排序的效率要比直接插入排序高。

+

希尔排序的分析是复杂的,时间复杂度是所取增量的函数,这涉及一些数学上的难题。但是在大量实验的基础上推出当n在某个范围内时,时间复杂度可以达到O(n^1.3)。

+

实现代码:

+
+
/**
+ *@Description:<p>希尔排序算法实现</p>
+ *@author 王旭
+ *@time 2016-3-3 下午10:53:55
+ */
+public class ShellSort {
+
+    /**
+     * 希尔排序的一趟插入
+     * @param arr 待排数组
+     * @param d 增量
+     */
+    public static void shellInsert(int[] arr, int d) {
+        for(int i=d; i<arr.length; i++) {
+            int j = i - d;
+            int temp = arr[i];    //记录要插入的数据  
+            while (j>=0 && arr[j]>temp) {  //从后向前,找到比其小的数的位置   
+                arr[j+d] = arr[j];    //向后挪动  
+                j -= d;  
+            }  
+
+            if (j != i - d)    //存在比其小的数 
+                arr[j+d] = temp;
+
+        }
+    }
+
+    public static void shellSort(int[] arr) {
+        if(arr == null || arr.length == 0)
+            return ;
+        int d = arr.length / 2;
+        while(d >= 1) {
+            shellInsert(arr, d);
+            d /= 2;
+        }
+    }
+
+}
+
+

归并排序

+

归并排序是另一种不同的排序方法,因为归并排序使用了递归分治的思想,所以理解起来比较容易。其基本思想是,先递归划分子问题,然后合并结果。把待排序列看成由两个有序的子序列,然后合并两个子序列,然后把子序列看成由两个有序序列。。。。。倒着来看,其实就是先两两合并,然后四四合并。。。最终形成有序序列。空间复杂度为O(n),时间复杂度为O(nlogn)。

+

举个栗子:

+

+

实现代码:

+
+
/**
+ *@Description:<p>归并排序算法的实现</p>
+ *@author 王旭
+ *@time 2016-3-4 上午8:14:20
+ */
+public class MergeSort {
+
+    public static void mergeSort(int[] arr) {
+        mSort(arr, 0, arr.length-1);
+    }
+
+    /**
+     * 递归分治
+     * @param arr 待排数组
+     * @param left 左指针
+     * @param right 右指针
+     */
+    public static void mSort(int[] arr, int left, int right) {
+        if(left >= right)
+            return ;
+        int mid = (left + right) / 2;
+
+        mSort(arr, left, mid); //递归排序左边
+        mSort(arr, mid+1, right); //递归排序右边
+        merge(arr, left, mid, right); //合并
+    }
+
+    /**
+     * 合并两个有序数组
+     * @param arr 待合并数组
+     * @param left 左指针
+     * @param mid 中间指针
+     * @param right 右指针
+     */
+    public static void merge(int[] arr, int left, int mid, int right) {
+        //[left, mid] [mid+1, right]
+        int[] temp = new int[right - left + 1]; //中间数组
+
+        int i = left;
+        int j = mid + 1;
+        int k = 0;
+        while(i <= mid && j <= right) {
+            if(arr[i] <= arr[j]) {
+                temp[k++] = arr[i++];
+            }
+            else {
+                temp[k++] = arr[j++];
+            }
+        }
+
+        while(i <= mid) {
+            temp[k++] = arr[i++];
+        }
+
+        while(j <= right) {
+            temp[k++] = arr[j++];
+        }
+
+        for(int p=0; p<temp.length; p++) {
+            arr[left + p] = temp[p];
+        }
+
+    }
+}
+
+

计数排序

+

如果在面试中有面试官要求你写一个O(n)时间复杂度的排序算法,你千万不要立刻说:这不可能!虽然前面基于比较的排序的下限是O(nlogn)。但是确实也有线性时间复杂度的排序,只不过有前提条件,就是待排序的数要满足一定的范围的整数,而且计数排序需要比较多的辅助空间。其基本思想是,用待排序的数作为计数数组的下标,统计每个数字的个数。然后依次输出即可得到有序序列。

+

实现代码:

+
+
/**
+ *@Description:<p>计数排序算法实现</p>
+ *@author 王旭
+ *@time 2016-3-4 下午4:52:02
+ */
+public class CountSort {
+
+    public static void countSort(int[] arr) {
+        if(arr == null || arr.length == 0)
+            return ;
+
+        int max = max(arr);
+
+        int[] count = new int[max+1];
+        Arrays.fill(count, 0);
+
+        for(int i=0; i<arr.length; i++) {
+            count[arr[i]] ++;
+        }
+
+        int k = 0;
+        for(int i=0; i<=max; i++) {
+            for(int j=0; j<count[i]; j++) {
+                arr[k++] = i;
+            }
+        }
+
+    }
+
+    public static int max(int[] arr) {
+        int max = Integer.MIN_VALUE;
+        for(int ele : arr) {
+            if(ele > max)
+                max = ele;
+        }
+
+        return max;
+    }
+
+}
+
+

桶排序

+

桶排序算是计数排序的一种改进和推广,但是网上有许多资料把计数排序和桶排序混为一谈。其实桶排序要比计数排序复杂许多。

+

对桶排序的分析和解释借鉴这位兄弟的文章(有改动):http://hxraid.iteye.com/blog/647759

+

桶排序的基本思想:

+

假设有一组长度为N的待排关键字序列K[1....n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素(每个桶B[i]都是一组大小为N/M的序列)。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]….B[M]中的全部内容即是一个有序序列。bindex=f(key) 其中,bindex 为桶数组B的下标(即第bindex个桶), k为待排序列的关键字。桶排序之所以能够高效,其关键在于这个映射函数,它必须做到:如果关键字k1<k2,那么f(k1)<=f(k2)。也就是说B(i)中的最小数据都要大于B(i-1)中最大数据。很显然,映射函数的确定与数据本身的特点有很大的关系。

+

举个栗子:

+

+

假如待排序列K= {49、 38 、 35、 97 、 76、 73 、 27、 49 }。这些数据全部在1—100之间。因此我们定制10个桶,然后确定映射函数f(k)=k/10。则第一个关键字49将定位到第4个桶中(49/10=4)。依次将所有关键字全部堆入桶中,并在每个非空的桶中进行快速排序后得到如图所示。只要顺序输出每个B[i]中的数据就可以得到有序序列了。

+

桶排序分析:

+

桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的f(k)值的计算,其作用就相当于快排中划分,希尔排序中的子序列,归并排序中的子问题,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做先进的比较排序即可。

+

对N个关键字进行桶排序的时间复杂度分为两个部分:

+

(1) 循环计算每个关键字的桶映射函数,这个时间复杂度是O(N)。

+

(2) 利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度为 ∑ O(Ni*logNi) 。其中Ni 为第i个桶的数据量。

+

很显然,第(2)部分是桶排序性能好坏的决定因素。尽量减少桶内数据的数量是提高效率的唯一办法(因为基于比较排序的最好平均时间复杂度只能达到O(N*logN)了)。因此,我们需要尽量做到下面两点:

+

(1) 映射函数f(k)能够将N个数据平均的分配到M个桶中,这样每个桶就有[N/M]个数据量。

+

(2) 尽量的增大桶的数量。极限情况下每个桶只能得到一个数据,这样就完全避开了桶内数据的“比较”排序操作。当然,做到这一点很不容易,数据量巨大的情况下,f(k)函数会使得桶集合的数量巨大,空间浪费严重。这就是一个时间代价和空间代价的权衡问题了。

+

对于N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:

+

O(N)+O(M*(N/M)*log(N/M))=O(N+N*(logN-logM))=O(N+N*logN-N*logM)

+

当N=M时,即极限情况下每个桶只有一个数据时。桶排序的最好效率能够达到O(N)。

+

总结: 桶排序的平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM)。如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)。 当然桶排序的空间复杂度 为O(N+M),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。

+

实现代码:

+
+
/**
+ *@Description:<p>桶排序算法实现</p>
+ *@author 王旭
+ *@time 2016-3-4 下午7:39:31
+ */
+public class BucketSort {
+
+    public static void bucketSort(int[] arr) {
+        if(arr == null && arr.length == 0)
+            return ;
+
+        int bucketNums = 10; //这里默认为10,规定待排数[0,100)
+        List<List<Integer>> buckets = new ArrayList<List<Integer>>(); //桶的索引
+
+        for(int i=0; i<10; i++) {
+            buckets.add(new LinkedList<Integer>()); //用链表比较合适
+        }
+
+        //划分桶
+        for(int i=0; i<arr.length; i++) {
+            buckets.get(f(arr[i])).add(arr[i]);
+        }
+
+        //对每个桶进行排序
+        for(int i=0; i<buckets.size(); i++) {
+            if(!buckets.get(i).isEmpty()) {
+                Collections.sort(buckets.get(i)); //对每个桶进行快排
+            }
+        }
+
+        //还原排好序的数组
+        int k = 0;
+        for(List<Integer> bucket : buckets) {
+            for(int ele : bucket) {
+                arr[k++] = ele;
+            }
+        }
+    }
+
+    /**
+     * 映射函数
+     * @param x
+     * @return
+     */
+    public static int f(int x) {
+        return x / 10;
+    }
+
+}
+
+

基数排序

+

基数排序又是一种和前面排序方式不同的排序方式,基数排序不需要进行记录关键字之间的比较。基数排序是一种借助多关键字排序思想对单逻辑关键字进行排序的方法。所谓的多关键字排序就是有多个优先级不同的关键字。比如说成绩的排序,如果两个人总分相同,则语文高的排在前面,语文成绩也相同则数学高的排在前面。。。如果对数字进行排序,那么个位、十位、百位就是不同优先级的关键字,如果要进行升序排序,那么个位、十位、百位优先级一次增加。基数排序是通过多次的收分配和收集来实现的,关键字优先级低的先进行分配和收集。

+

举个栗子:

+

+

+

实现代码:

+
+
/**
+ *@Description:<p>基数排序算法实现</p>
+ *@author 王旭
+ *@time 2016-3-4 下午8:29:52
+ */
+public class RadixSort {
+
+    public static void radixSort(int[] arr) {
+        if(arr == null && arr.length == 0)
+            return ;
+
+        int maxBit = getMaxBit(arr);
+
+        for(int i=1; i<=maxBit; i++) {
+
+            List<List<Integer>> buf = distribute(arr, i); //分配
+            collecte(arr, buf); //收集
+        }
+
+    }
+
+    /**
+     * 分配
+     * @param arr 待分配数组
+     * @param iBit 要分配第几位
+     * @return
+     */
+    public static List<List<Integer>> distribute(int[] arr, int iBit) {
+        List<List<Integer>> buf = new ArrayList<List<Integer>>();
+        for(int j=0; j<10; j++) {
+            buf.add(new LinkedList<Integer>());
+        }
+        for(int i=0; i<arr.length; i++) {
+            buf.get(getNBit(arr[i], iBit)).add(arr[i]);
+        }
+        return buf;
+    }
+
+    /**
+     * 收集
+     * @param arr 把分配的数据收集到arr中
+     * @param buf 
+     */
+    public static void collecte(int[] arr, List<List<Integer>> buf) {
+        int k = 0;
+        for(List<Integer> bucket : buf) {
+            for(int ele : bucket) {
+                arr[k++] = ele;
+            }
+        }
+
+    }
+
+    /**
+     * 获取最大位数
+     * @param x
+     * @return
+     */
+    public static int getMaxBit(int[] arr) {
+        int max = Integer.MIN_VALUE;
+        for(int ele : arr) {
+            int len = (ele+"").length();
+            if(len > max)
+                max = len;
+        }
+        return max;
+    }
+
+    /**
+     * 获取x的第n位,如果没有则为0.
+     * @param x
+     * @param n
+     * @return
+     */
+    public static int getNBit(int x, int n) {
+
+        String sx = x + "";
+        if(sx.length() < n)
+            return 0;
+        else
+            return sx.charAt(sx.length()-n) - '0';
+    }
+
+}
+
+

总结

+

在前面的介绍和分析中我们提到了冒泡排序、选择排序、插入排序三种简单的排序及其变种快速排序、堆排序、希尔排序三种比较高效的排序。后面我们又分析了基于分治递归思想的归并排序还有计数排序、桶排序、基数排序三种线性排序。我们可以知道排序算法要么简单有效,要么是利用简单排序的特点加以改进,要么是以空间换取时间在特定情况下的高效排序。但是这些排序方法都不是固定不变的,需要结合具体的需求和场景来选择甚至组合使用。才能达到高效稳定的目的。没有最好的排序,只有最适合的排序。

+

下面就总结一下排序算法的各自的使用场景和适用场合。

+

+

1. 从平均时间来看,快速排序是效率最高的,但快速排序在最坏情况下的时间性能不如堆排序和归并排序。而后者相比较的结果是,在n较大时归并排序使用时间较少,但使用辅助空间较多。

+

2. 上面说的简单排序包括除希尔排序之外的所有冒泡排序、插入排序、简单选择排序。其中直接插入排序最简单,但序列基本有序或者n较小时,直接插入排序是好的方法,因此常将它和其他的排序方法,如快速排序、归并排序等结合在一起使用。

+

3. 基数排序的时间复杂度也可以写成O(d*n)。因此它最使用于n值很大而关键字较小的的序列。若关键字也很大,而序列中大多数记录的最高关键字均不同,则亦可先按最高关键字不同,将序列分成若干小的子序列,而后进行直接插入排序。

+

4. 从方法的稳定性来比较,基数排序是稳定的内排方法,所有时间复杂度为O(n^2)的简单排序也是稳定的。但是快速排序、堆排序、希尔排序等时间性能较好的排序方法都是不稳定的。稳定性需要根据具体需求选择。

+

5. 上面的算法实现大多数是使用线性存储结构,像插入排序这种算法用链表实现更好,省去了移动元素的时间。具体的存储结构在具体的实现版本中也是不同的。

+

附:基于比较排序算法时间下限为O(nlogn)的证明:

+

基于比较排序下限的证明是通过决策树证明的,决策树的高度Ω(nlgn),这样就得出了比较排序的下限。

+

+

首先要引入决策树。 首先决策树是一颗二叉树,每个节点表示元素之间一组可能的排序,它予以京进行的比较相一致,比较的结果是树的边。 先来说明一些二叉树的性质,令T是深度为d的二叉树,则T最多有2^片树叶。 具有L片树叶的二叉树的深度至少是logL。 所以,对n个元素排序的决策树必然有n!片树叶(因为n个数有n!种不同的大小关系),所以决策树的深度至少是log(n!),即至少需要log(n!)次比较。 而 log(n!)=logn+log(n-1)+log(n-2)+…+log2+log1 >=logn+log(n-1)+log(n-2)+…+log(n/2) >=(n/2)log(n/2) >=(n/2)logn-n/2 =O(nlogn) 所以只用到比较的排序算法最低时间复杂度是O(nlogn)。

+

参考资料:

+
    +
  • 《数据结构》 严蔚敏 吴伟民 编著
  • +
  • 桶排序分析:http://hxraid.iteye.com/blog/647759
  • +
  • 部分排序算法分析与介绍:http://www.cnblogs.com/weixliu/archive/2012/12/23/2829671.html
  • +
+ + + + +
+ + + +