在执行main
函数的时候,如果运行过程中遇到异常问题,有两种情况:
try-catch
捕获已知或者未知的异常将问题处理并跳过,然后继续运行,确保程序不会崩溃Thread.run()
或主线程抛出后就不能在程序中对异常进行捕获,最终只能由JVM捕获由JVM来处理JVM 有一个默认的异常处理机制,遇到异常,抛出异常,和打印异常信息,同时将程序停止运行,这就是我们看到的程序崩溃。
Java 的Thread
类中有一个UncaughtExceptionHandler
接口,该接口的作用主要是为了当Thread因未捕获的异常而突然终止时,调用处理程序处理异常。
1 | //UncaughtExceptionHandler接口唯一的回调函数 |
JVM 遇到线程未捕获的异常后,通过 Thread 的dispatchUncaughtException(e)
方法分发异常到当前线程:
1 | // art/runtime/thread.cc |
这个java_lang_Thread_dispatchUncaughtException
方法就是 Thread 中的dispatchUncaughtException
方法的缓存:
1 | //art/runtime/well_known_classes.cc |
Thread 的dispatchUncaughtException
方法如下:
1 | public final void dispatchUncaughtException(Throwable e) { |
这里有 2 个UncaughtExceptionHandler
会参与处理,分别是PreHandler
和Handler
,核心是执行其各自实现的uncaughtException
方法。
Android 中提供了此二者的默认实现。Android 系统中,应用进程由Zygote进程孵化而来,Zygote进程启动时,zygoteInit
方法中会调用RuntimeInit.commonInit
,代码如下:
1 | // frameworks/base/core/java/com/android/internal/os/ZygoteInit.java |
在commonInit
方法中实例化了 2 个对象,分别是LoggingHandler
和KillApplicationHandler
,均实现了Thread.UncaughtExceptionHandler
接口。其中:
LoggingHandler
负责打印异常信息,包括进程名,pid,Java栈信息等"*** FATAL EXCEPTION IN SYSTEM PROCESS: "
开头"FATAL EXCEPTION: "
开头KillApplicationHandler
检查日志是否已打印,通知 AMS 应用 Crash,并杀死当前进程。注意1:
UncaughtHandler
类LoggingHandler
和KillApplicationHandler
,回调方法uncaughtException
实现如下:1 | public void uncaughtException(Thread t, Throwable e) { |
注意2:
Thread.setDefaultUncaughtExceptionHandler
是公开 API。应用可通过调用自定义UncaughtExceptionHandler
,替换掉KillApplicationHandler
,这样能自定义逻辑处理掉异常,避免闪退发生
Thread.setUncaughtExceptionPreHandler
是 hidden API。应用不能直接调用,确保异常发生时能够正常打印异常日志,参考Thread.java
的更新日志:
1 | Add a new @hide API to set an additional UncaughtExceptionHandler that is called before dispatching to the regular handler. The framework uses this to enforce logging. |
Android O 及以后版本,对于任何一个线程异常,会优先经过getUncaughtExceptionPreHandler
方法获取异常预处理器处理, 然后通过getUncaughtExceptionHandler
方法获取当前线程实例的异常处理器处理异常。
Thread 的getUncaughtExceptionHandler
方法:
1 | public UncaughtExceptionHandler getUncaughtExceptionHandler() { |
如果当前线程没有设置异常处理器,会选择当前线程所在的ThreadGroup
(ThreadGroup 是一个Thread 的集合,自己实现了UncaughtExceptionHandler
接口)来处理异常:
1 | public void uncaughtException(Thread t, Throwable e) { |
在ThreadGroup
的uncaughtException
回调中会通过getDefaultUncaughtExceptionHandler
接口获取默认的线程异常处理器进行最后的异常处理。
综上所述,当JVM遇到未捕获的异常时:
异常预处理器
处理异常预处理器
预处理后交给当前线程的异常处理器
处理ThreadGroup
来处理父线程组
处理,依次向上委托根线程组
中获取线程共有的默认异常处理器
来处理异常以上流程总结如下图所示:
注意:
Android 中如果我们仅仅通过setDefaultUncaughtExceptionHandler
方法覆盖默认的异常处理器,在回调中收集异常信息时,一定要注意记得杀死当前进程(让它痛快的死去):
1 | Process.killProcess(Process.myPid()); |
不然应用就会陷入卡死状态,无法响应界面操作,进入了生不如死的状态。
通过上面的分析,我们知道出现 Crash 时,JVM 是通过Thread::HandleUncaughtExceptions
方法将异常从Native 层传递到Java 层来逐层分发处理。
那么HandleUncaughtExceptions
方法这个方法到底是在哪里调用的呢?搜索整个 Android 源码,我们只能找到一处调用,也即:
1 | void Thread::Destroy() { |
这个Destroy()又是在什么时候调用呢?
通过搜索自然能够找到,如下图所示:
但不够直观理解。这里有一份 Crash 后打印的 Native 层堆栈信息(这个堆栈信息平时应该比较常见):
1 | #23 pc 0000000000389c19 /system/lib/libart.so (art::Thread::HandleUncaughtExceptions(art::ScopedObjectAccessAlreadyRunnable&)+280) |
其中art::Thread::CreateCallback
方法如下:
1 | void* Thread::CreateCallback(void* arg) { |
Android Java 中的Thread
类通过 start 启动一个线程时,会通过一个 native 函数nativeCreate
进入 jni 层完成真正的线程创建:
1 | public synchronized void start() { |
这个nativeCreate
方法接着会调用到art::Thread::CreateNativeThread
方法:
1 | // art/runtime/native/java_lang_Thread.cc |
JVM 在art::Thread::CreateNativeThread
方法中通过pthread_create
创建 Native 层的线程,并回调CreateCallback
接口。
1 | void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) { |
CreateCallback
接口中有一段{}
括起来的代码块中调用了 Java 层Thread
的run
方法,进入 java 层Thread
的线程循环。
1 | void* Thread::CreateCallback(void* arg) { |
正常情况下对于主线程而言这里的run
方法里会进入死循环,也就是当前的主线程ActivityThread
的 main 函数中的Loop.loop()
。
一旦主线程中出现未捕获的异常,就会跳出主线程循环,从而离开这里的代码块,回调art::ThreadList::Unregister
方法,然后调用art::Thread::Destroy
方法,最后通过HandleUncaughtExceptions
方法分发异常, 这也正好与上文中的异常堆栈吻合。
现在又问题来了,为什么出现异常就会退出这个线程循环呢?
这个问题要从 Java 的字节码指令执行上说起,首先我们举个简单的例子,crash()
方法中触发一个简单的除零异常:
1 | public class Crash { |
其中crash
方法的smali
代码如下:
1 | .method public static crash()V |
关于 Java 代码在dalvik与art中的执行,这里暂不详细展开。这个除法操作编译后转换为了一条div-int
指令,当虚拟机需要执行这个语句时,首先会去解释这个语句,通过字符串匹配的形式找到对应的指令代码,这条语句对应DIV_INT_LIT8
方法
1 | //art/libdexfile/dex/dex_instruction_list.h |
DIV_INT_LIT8
方法继而调用DoIntDivide
方法:
1 | // art/runtime/interpreter/interpreter_switch_impl-inl.h |
DoIntDivide
方法定义如下:
1 | // art/runtime/interpreter/interpreter_common.h |
这里我们可以看到当被除数divisor
等于0时,就通过ThrowArithmeticExceptionDivideByZero
方法抛出除零异常,继续跟踪:
1 | // art/runtime/common_throws.cc |
进入art::Thread::ThrowNewWrappedException
方法后,会进行一大堆操作,包括获取当前线程的堆栈,最后赋值给tlsPtr
这个大的结构体的exception
。
也就是说当虚拟机一步一步执行 Java 指令的时候,当遇到类似除零这种异常操作时,就会抛出一个对应的异常,然后一步一步返回到当前执行的地方,将异常入栈,跳出当前指令循环(可能解释得不是很清楚,参考这里),也就是结束了 Java 层的线程循环,回到art::Thread::CreateCallback
回调中,从而进行接下来的异常分发流程。也就是说并不是虚拟机遇到未知运算或者未知指令出现了不可预期的异常,而是知道这个操作不符合规范,给不了有效的结果,主动抛出来一个异常给应用层。
首先我们平时通过apply plugin: 'com.android.application'
引用的android这个插件的源码我们可以从aosp代码仓库里面找到,传送门(这里给的是android-10.0.0_r11的,历史版本可以自行查找,需要fan qiang)
比如下面firebase
用到的google-service
插件也可以从aosp中传送门找到
apply plugin: 'com.google.gms.google-services'
接着就是一些商用或者非商用sdk使用到的gradle插件了,往往我们比较好奇这个插件里面到底干了啥,或者刚好它有个好用的功能我也想借鉴一下,那么看其源码肯定是最直接好用的办法。
下面举个例子,比如:ShareSDK
打开项目根目录的build.gradle,在buildscrip–>dependencies 模块下面添加
1 | classpath ‘com.mob.sdk:MobSDK:2018.0319.1724’,如下所示; |
在使用到Mob产品的module下面的build.gradle文件里面添加引用
1 | apply plugin: 'com.mob.sdk' |
External Library
下面找找看是否有ShareSDK他家的sdk引入,如下表示引入成功。com.mob.sdk
,那么这个插件就一定缓存到我们PC本地了,所以用这个名字搜一下即可(Windows下有Everything这个工具简直完美), 很快就能在gradle的缓存目录找到这个插件,如下图(我这里修改了gradle的默认位置,Windows一般会在C盘:C:\Users\xxx\.gradle\caches\modules-2\files-2.1
下面,Mac一般在/Users/xxx/.gradle/caches/modules-2/files-2.1
),xxx-source.jar
的文件,这个就是插件源码打成的jar包,实际就是个压缩文件,包含了插件groovy源码,解压即可,源码带注释真香ing 。type
属性值,"password"
改为"text"
,回车正常情况下查看浏览器保存的账号密码需要输入系统密码或者验证浏览器的某项设定(不同浏览器有差异),但此方法轻松的规避了权限验证获取到密码。这种方便用户的行为却带来了极大的密码泄露风险,根源上无法规避,只能在平时使用电脑时多注意一下,以下几点仅供参考:
冒泡
、插入(对半插入)
、选择
、希尔
、堆排序
、归并
、快速
、七种排序算法
,收集了网上的示意图、动图进行生动的展示。由于内容较多,这里只提供了
Java
代码int类型数组进行举例说明,其他语言或者数据类型请自行转换。
首先针对排序常用到的数据交换Swap函数,汇总了一下它的三种实现方式:
1 | /** |
1 | /** |
1 | /** |
排序方式 | 时间复杂度 | 空间复杂度 | 稳定性 | ||
---|---|---|---|---|---|
平均 | 最好 | 最坏 | 平均 | ||
冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
插入排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
希尔排序 | O(n¹·³) | O(n) | O(n²) | O(1) | 不稳定 |
堆排序 | O(n㏒₂n) | O(n㏒₂n) | O(n㏒₂n) | O(n) | 稳定 |
归并排序 | O(n㏒₂n) | O(n㏒₂n) | O(n㏒₂n) | O(n) | 稳定 |
快速排序 | O(n㏒₂n) | O(n㏒₂n) | O(n²) | O(n㏒₂n) | 不稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | 稳定 |
桶排序 | O(n+k) | O(n) | O(n²) | O(n+k) | 稳定 |
基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | 稳定 |
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的一端。
根据冒泡的方向,可以分为前向冒泡和后向冒泡,对于升序排序:前向冒泡指较小的数逐渐浮到数列的前方,后向冒泡指较大的数逐渐后浮。降序排序则反之。
前向冒泡
后向冒泡
1 | /** |
根据冒泡排序的原理,若原数组本身就是有序的(这是最好情况),仅需n-1次比较就可完成;若是倒序,比较次数为 n-1+n-2+…+1=n(n-1)/2,交换次数和比较次数等值。所以,其时间复杂度依然为O(n²)。
直接插入排序基本思想是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。
1 | /** |
插入排序在最好情况下,需要比较n-1次,无需交换元素,时间复杂度为O(n);在最坏情况下,时间复杂度依然为O(n²)。
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
1 | /** |
1959年Shell发明,第一个突破O(n²)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
1 | /** |
希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(n㏒₂n),它也是不稳定排序。首先简单了解下堆结构。
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:
同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了.
步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
a.假设给定无序序列结构如下
b.此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。
c.找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。
d.这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。
此时,我们就将一个无需序列构造成了一个大顶堆。
步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
a.将堆顶元素9和末尾元素4进行交换
b.重新调整结构,使其继续满足堆定义
c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
再简单总结下堆排序的基本思路:
a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大元素”沉”到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
1 | /** |
堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[㏒₂(n-1),㏒₂(n-2)…1]逐步递减,近似为n㏒₂n。所以堆排序时间复杂度一般认为就是O(n㏒₂n)级。
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案”修补”在一起,即分而治之)。
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
1 | /** |
n个元素都要被遍历一遍以保证其被放到新数组, 需要将待排序序列中的所有记录扫描一遍,所以O(n)。 由完全二叉树可知,整个归并排序需要 ㏒₂n次, 所以 最好=最坏=平均=O(n㏒₂n)。 由于归并排序需要与原始记录序列同样数量的存储空间存放归并结果以及递归时㏒₂n的栈空间, 所以空间复杂度O(n+㏒₂n)。
归并排序是稳定排序,它也是一种十分高效的排序,能利用完全二叉树特性的排序一般性能都不会太差。java中Arrays.sort()采用了一种名为TimSort的排序算法,就是归并排序的优化版本。从上文的图中可看出,每次合并操作的平均时间复杂度为O(n),而完全二叉树的深度为|㏒₂n|。总的平均时间复杂度为O(n㏒₂n)。而且,归并排序的最好,最坏,平均时间复杂度均为O(n㏒₂n)。
快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。
1 | /** |
快速排序是一种交换类的排序,它同样是分治法的经典体现。在一趟排序中将待排序的序列分割成两组,其中一部分记录的关键字均小于另一部分。然后分别对这两组继续进行排序,以使整个序列有序。在分割的过程中,枢纽值的选择至关重要,本文采取了三位取中法,可以很大程度上避免分组”一边倒”的情况。快速排序平均时间复杂度也为O(n㏒₂n)级。
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
1 | (略) |
计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
1 | (略) |
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
1 | (略) |
基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。
排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。
其次,说一下稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,对基于比较的排序算法而言,元素交换的次数可能会少一些(个人感觉,没有证实)。
回到主题,现在分析一下常见的排序算法的稳定性,每个都给出简单的理由。
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n - 1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j,交换a[i]和a[j],重复上面的过程,直到i > j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为5 3 3 4 3 8 9 10 11,现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。
归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定是是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比O(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
我们知道堆的结构是节点i的孩子为2 * i和2 * i + 1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n / 2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n / 2 - 1, n / 2 - 2, … 1这些个父节点选择元素时,就会破坏稳定性。有可能第n / 2个父节点交换把后面一个元素交换过去了,而第n / 2 - 1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
综上,得出结论: 选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序是稳定的排序算法。
常见排序算法总结与实现(冒泡、插入、选择、希尔、堆排序、归并、快排):https://www.cnblogs.com/alsf/p/6606287.html
十大经典排序算法(动图演示):https://www.cnblogs.com/onepixel/articles/7674659.html
图解排序算法(一)之3种简单排序(选择,冒泡,直接插入)
:http://www.cnblogs.com/chengxiao/p/6103002.html
图解排序算法(三)之堆排序:http://www.cnblogs.com/chengxiao/p/6129630.html
图解排序算法(四)之归并排序:https://www.cnblogs.com/chengxiao/p/6194356.html
图解排序算法(五)之快速排序——三数取中法:http://www.cnblogs.com/chengxiao/p/6262208.html
]]>阅读本文需要对docker有一定的了解,以及会一些基本的使用。
随着docker越来越流行,很多公司内部linux机器上docker成了标配。
默认情况下使用docker必须要有sudo权限,对于一台机器多用户使用,往往很多用户只有普通权限,如何保证普通用户也能顺利使用Docker呢?
这一点想必难不到大家,只需要管理员将需要使用docker的用户添加到docker用户组
(安装docker后默认会创建该组)中,用户重新登录机器即可免sudo
使用docker了。
我们知道,用户创建一个docker容器后,大多情况下容器内默认是root用户,在不需要加sudo的情况下可以任意更改容器内的配置。
正常情况下,这种模式既可以保证一台机器被很多普通用户使用,通过docker容器的隔离,相互之前互不影响;也给用户在容器内开放了充足的权限保证用户可以正常安装软件,修改容器配置等操作以满足个性化使用。
在我们创建容器的时候,docker提供了一个-v
选项,用于用户将容器外的host目录映射进容器内,方便的进行容器内外的文件共享。
然而便利倒是有了,但潜在了风险也是随之而来。
结合上面的两点便利,笔者想到一种普通用户借助docker突破host上权限的限制,达到本地提权的目的。参见下图:
初始情况下这里host上的test
用户是非sudo组用户,只拥有普通权限。为了免sudo使用docker,已提前通过管理员将test用户加入docker用户组(见id
命令结果)。
首先我们借助任意一个docker镜像创建一个容器:
1 | docker run -it --rm xxx /bin/bash#常规使用 |
一般情况下我们如果不需要与host上进行文件共享,就可以不用加-v
参数进行文件或目录映射,或者极大多数场景下加个人工作目录映射进容器内,便于容器内操作host上的文件,或者拷贝文件到host。
为了达到提权的目的,这里最关键的一点就是-v
选项的参数/etc:/etc
。
我们知道linux机器上的本地用户信息主要记录在/etc/
目录下,比如常见文件/etc/passwd
、/etc/shadow
和/etc/group
,这几个文件分别记录了用户基本属性、用户密码与用户分组信息。
正常情况下创建的容器,内部也会有/etc目录,容器内部的用户信息也是记录在该目录。
然而我这里巧妙的将host上的/etc目录直接映射进容器,从而覆盖了容器内的/etc目录。再加上容器内用户默认是root,拥有超级管理员权限,如上图中,通过容器内的root用户在容器内新加了一个用户test1:adduser test1
,并赋予该用户sudo权限:usermod -aG sudo test1
。
至此,为了提权docker容器的作用已结束,Ctrl+D退出容器回到host,通过cat /etc/passwd
查看一下本地用户基本属性,想必大家也能猜出这么做会出现什么有趣的现象。图中我没有查看,而是直接su test1
,顺利切换到在容器内添加的用户,也就是说在容器内添加的用户实际上也添加到了host上。通过id
命令查看该用户也被同步加入到了sudo用户组。
其实到了这一步已经达到了提权的目的,即通过“普通的test”用户借助docker容器成功创建了一个具有sudo权限的用户test1。图中只是增加了一步,借助test1将test也加入sudo用户组,其实效果一样。
当然这里为了简化操作也可以不用增加test1用户,直接在容器内将test用户加入sudo用户组,因为此时test用户对于容器也是可见,也是可以直接操作其所属群组,如下图所示:
最后在host上也可以看到test用户已经被加入sudo用户组了。此时我没有用id命令查看的一个原因是,linux的shell并没有自动更新当前用户信息,可以退出Terminal重新进入。
注:此时如果没有重新连接,test用户还是无法使用sudo命令。
在这种多用户借助docker共用一台机器的情况下,“普通用户”可以轻松的借助docker提升为sudo用户,从而可以进行任意修改系统配置等各种恶意操作。
以上这种情形本地用户的破坏还不是很明显,毕竟是公司内部用户大多不会进行恶意操作。然而,很多情况下普通用户为了方便,用户密码往往设置得很简单,如果攻击者通过其他途径进入内网并暴力破解普通用户的弱口令,就可以很轻松的提升为管理员从事不可限制的恶意操作,这也大大降低了攻击者的攻击难度。
docker创建容器默认是以root身份来创建的,普通用户之所以能够创建容器的原因就是这个docker用户组,所以我们应该规避使用这种做法。对于多个用户想使用容器,可以通过管理员集中创建开启了ssh服务的容器,并提供端口映射到host上,让普通用户通过ssh登录指定端口进入容器,这样就可以限制普通用户的活动范围在容器内,用户的任意操作也不会扩散到host上,从而保障host机器的安全。
当然对于突破docker容器的攻击得另当别论,以其他方式来防御。
]]>最近在浏览网页的时候,感觉有些网站的广告真的很烦,一不好看影响视觉,二总在切图吸引注意力,三有些广告有点少儿不宜,于是琢磨该如何过滤广告呢?
起初这个想法源自于公司办公的时候。由于公司访问外网必须要配置代理,而在配置代理的地方有个高级设置,里面可以通过精确/模糊匹配的方式过滤一些网址,本来这个功能平时一直用于过滤公司内部的一些服务器或内部网址等,避免也走代理导致无法访问。
既然这功能能够过滤内网不走代理,那么是不是也可以过滤广告不走代理呢?
抱着试一试的心态,凭借着一点点网页调试的技能,抓到一个广告的网址(后面介绍如何抓广告网址),将它的host地址塞到代理过滤列表里,保存,然后刷新网页,广告蹦不出来啦嘚瑟~( ̄▽ ̄~)(~ ̄▽ ̄)~嘚瑟
见下图:
图1为本地代理设置入口,对于Windows系统一般很多浏览器的代理都可以配置使用Internet Explorer的代理,因此这里配置可以多其他浏览器都生效。图2中勾选”跳过本地地址的代理服务器”,点击”高级”按钮进入图3,在”例外情况”输入框中填入需要过滤的广告网址host。
注:以上方法对于公司通过代理上外网有效。
于是,又去几个有广告的页面,分别抓到他们的广告网址纷纷丢到过滤列表了,QND,有的网页同一个广告挂了三四个,哥全给你毙了,顿时网页清净多了~
以下针对Chrome浏览器简单介绍,其他浏览器类似:
如图,CSDN博客左侧中与右下角的广告是我们本次的目标:
在 菜单》工具》开发者工具 打开开发人员工具
辅助面板,依次展开里面的body
以及多级div
标签,在展开的过程中通过在不同的div移动,观察上面网页上浅蓝色蒙版当前定位,刚好盖在广告上时就说嘛找到了,从而快速找到是那个div下面有广告链接,如下图:
高亮的div
即是我们要找的位置,注意到里面有个src=//cee1.iteye.com/lgyyovfyh.js
的链接,OK,其中的cee1.iteye.com
就是广告链接的host
地址,依次类推,本页面右下角的广告host为pos.baidu.com
。不过,本页面的作者比较厚道,代码中直接有注释<!--投放代码-->
、<!--右下角弹窗广告-->
,有助于快速找到广告的标签。
通过以上方式配合公司代理过滤,就可以达到过滤广告的目的,以后遇到一个抓一个,从此网页浏览一片清净~
但是回到家,不需要公司代理,该如何过滤广告呢?用第三方软件、插件?难选!效果不佳!不靠谱!!!不靠谱!!!不靠谱!!!(重要的事说三遍
)
搞一个代理服务器?太麻烦!
那该怎么办呢?
想到以前没有代理又想用google的一点小技能:改hosts访问google
,方法这里不介绍了,读者可自行网上搜一下。
这里copy一下hosts的原理
(源于百度百科):
hosts是一个没有扩展名的系统文件,可以用记事本等工具打开,其作用就是将一些常用的网址域名与其对应的IP地址建立一个关联“数据库”,当用户在浏览器中输入一个需要登录的网址时,系统会
首先自动从Hosts文件中寻找对应的IP地址
,一旦找到,系统会立即打开对应网页,如果没有找到,则系统会再将网址提交DNS域名解析服务器进行IP地址的解析。
需要注意的是,Hosts文件配置的映射是静态的
,如果网络上的计算机更改了请及时更新IP地址,否则将不能访问。
概括一下就是手动给浏览器一个域名—IP
的配置表,让它从配置中快速找到域名对应的IP地址,而不是去DNS服务器查找,从而直接访问目标服务器。
既然我们可以通过改hosts指引
浏览器去访问一个网站,那么也可以"误导"
它,_告诉它一个错误的IP,让它找不到广告服务器
,从而实现屏蔽广告
的目的_。
那么该告诉浏览器什么地址比较合适呢,这个就比较简单了,随便选一个不常用的IP,ping一下,ping不通就可以了,比如:1.1.1.1,2.2.2.2等等,随便啦!
将上文中介绍的找到的广告host,配合选好的错误的IP
地址填入系统hosts配置文件,格式如下,追加到hosts配置文件末尾,保存即可(可能需要管理员权限
)。
1 | #百度 |
如果有部分广告仍旧冒出来了,可尝试更换IP试试,本人试了0.0.0.0对于有些广告不管用,有些管用,不知为啥。
一点小技巧,需要长期积累收集广告链接。另外对于一些应然软件中的广告,可能就需要一点网络抓包的技能了,这里就不展开了,祝上网愉快~
Windows:C:\Windows\System32\drivers\etc\hosts
Linux:/etc/hosts
macOS:/etc/hosts
1 | #AD Block Start |
该漏洞存在于带有 eBPF bpf(2)系统(CONFIG_BPF_SYSCALL)编译支持的Linux内核中,是一个内存任意读写漏洞。该漏洞是由于eBPF
验证模块的计算错误产生的。普通用户可以构造特殊的BPF来触发该漏洞,此外恶意攻击者也可以使用该漏洞来进行本地提权操作。
原作者exp此处可下载(可能需要梯子,这里copy了一份),然而直接运行,很多机器是无法提权成功的。
源代码注释头有说到:
if different kernel adjust CRED offset + check kernel stack size
针对这个魔鬼数字:CRED_OFFSET=0x5f8
这篇文章也说明了真相:
cred结构体的偏移量可能因为内核版本不同、内核编译选项不同而出现差异,作者给的exp偏移量是写死的
此文作者也给出了一种应对之策:
通过以下方法可获取这个cred offset
:
1 |
|
1 | obj-m += getCredOffset.o |
1 | make |
1 | sudo insmod getCredOffset.ko |
该命令需要有sudo权限的用户执行,通过insmod
命令将getCredOffset
模块注入内核
1 | dmesg | grep "cred offset" |
另开一个命令行执行该命令即可获取到cred offset
,最后替换掉原exp
中的偏移量
即可成功提权。
然而,虽提权成功了,但此法有点怪异,本来想普通用户提权,但却需要用root用户执行命令来协助,有点力不从心。
那么问题又来了,该如何在不同的机器上动态获取这个cred offset
呢?
经过上文作者的点拨:
这个漏洞是个任意地址读写漏洞,所以也可以在确定task_struct地址之后,以当前用户的uid为特征去搜索内存,毕竟cred离task_struct不远。
加上代码中有多处__read
命令,以及getuid()
命令,这两个命令都可以读取uid
。首先想到的是在往uidptr
对应的地址中写0
之前获取此时的uid
值,通过以上两种方式对比看有什么差异:
1 | printf("uidptr = %lx\n", uidptr); |
果然如下图所示:
那么规律来了,我们可以尝试以不同的`cred offset`来获取两个`uid`来进行对比,一旦对比上,姑且就当做找到了这个“确定”的值,然后再去`write(0)`。修改`pwn`函数如下:1 | static void pwn(uint64_t credoffset) { |
想到原作者Vitaly Nikolenko
给的CRED_OFFSET=0x5f8
,我这边通过rebeyond
这里给出的方法获取的是0x670
,猜测这个值应该范围不大,尝试了一下用0x400~0x800
爆破,很不幸,第一次尝试失败,被系统给killed掉啦:
调整一下范围:0x500~0x800,ok 搞定!
不同机器此CRED_OFFSET偏移量可能还有差异,可以
视情况稍微调整一下范围
,试出结果应该不难。
最后来体验一把提权后带来的快感,root用户想干嘛干嘛,如图:
完整代码见这里。
]]>参考链接:
bit15: 1 bit SIGN +---+-------+--------------+bit14-10: 5 bit EXP | S | EEEEE | MM MMMM MMMM |bit0-9: 10 bit MAN +---+-------+--------------+
参考 IEEE754-2008 WIKIPEDIA:Half-precision floating-point format
Hall-precision floating-point number 半精度浮点数,文中简称fp16
E=00000
$ y=(-1)^S\times \frac{M}{2^{10}} \times 2^{-14}$
E=00001
~11110
$y=(-1)^S\times(1+\frac{M}{2^{10}}) \times 2^{E-15}$
E=11111
M=00000 00000
E=11111
M>0
例如:9.125
9->1001
$2^3+1$
0.125->0.001
$2^{-3}$
9.125->1001.001
1001.001
->$1.001001\times2^3$
fp16总共16bit
,其中尾数10bit
,由于任何一个数(0除外)转换为二进制科学计数法
后,整数部分一定是1
,所以该bit可以不表示,及隐藏1个bit
,用10bit尾数表示小数部分
因此将小数部分(尾数)补齐到10bit为:00100 10000
fp16中指数位占5bit
,可以表示0~31
,为了让fp16既能表示整数,也能表示小数,我们给指数E加一个bias=15
,将-15~16
扩充到0~31
所以3+15=18:10010
指数占1bit
,用0表示整数
,1表示负数
综上:9.125
表示为fp16对应的二进制值为:0
10010
00100 10000
符号:S=0
指数:E=b10010
=18
尾数:M=b00100 10000
=144
浮点数值:$y=(-1)^0\times(1+\frac{144}{2^{10}})\times2^{18-15}=1.140625\times8=9.125$
Hexo
时,发现CSDN文章一个比较好玩的东东就是可以在内容中加入QQ表情,于是想弄到Hexo里面。 一开始想到的做法是把QQ表情下载下来,上传到七牛,然后用Markdown
插入图片功能插入图片。
看了一下生成后网页的源代码,如下:
估计是Fancybox
搞的鬼,它对html
格式img标签
做了什么处理吧,这种简单的做法在Hexo
里不可取,只能另寻方案!
还好以前接触过一点点css
,稍微懂得一点点,可以用background:url(xxx)
的方式将图片作为标签文字的背景嵌入,这样表情就不能点击。
首先想到的是a标签
,于是自定义了几个样式,用于不同的QQ表情,分别引用不同的表情url。(Hexo主题自定义css样式,见文末)
1 | #自定义CSS |
1 | #Markdown正文中引入a标签插入表情 |
网页预览一下,貌似还可以,如下图:
注:上图中使用了两个空格占位符,不然表情没法显示(PS:没有更好的办法,如有大神,请不吝指导一下),本文中段落首行缩进也是用了三个空格占位符     
不过,看着总觉得有点怪怪的,文字下面那一横线看着有点不爽,怎么把它去掉呢?
想到几年前自己复制hao123网址大全自己做了一个个人导航网页,如下每个文字前面的图标,用到的是i标签
实现的:
于是,尝试将方案二中的a标签
换成i标签
,
1 | #自定义CSS |
1 | #Markdown正文中引入i标签插入表情 |
效果如下:
生气表情前面的文字 表情后面的文字
哭泣表情前面的文字 表情后面的文字
那讨厌的横线终于没了,大功告成!!!
这里有篇文章讲得比较详细,我简单重复啰嗦一下。
本博用的是Hexo
的NexT-Mist
主题,找到博客themes\next\source\css
目录下的main.styl
文件,在文末加入一行@import "_myCss/myCss";
,然后在css
目录下新建一个_myCss
目录,并新建一个文本文件,保存为myCss.styl
文件,编辑该文件即可自定义各种css样式啦,祝使用愉快~
]]>
COUNT=0;dir=$(eval pwd); for name in $(ls $dir);do COUNT=$(($COUNT+1));param=$(eval printf "%03d" $COUNT);mv $name frame$param.png; done
将当前目录下所有图片文件按顺序格式化递增序号重命名
输出文件格式:xxx%03d.png
说明:$(eval pwd)
:获取当前目录for name in $(ls $dir)
:遍历当前目录下所以文件COUNT=$(($COUNT+1))
:计数器+1param=$(eval printf "%03d" $COUNT)
:计数器三位前向补零格式化mv $name frame$param.png
:文件重命名
Xcode
(include git
)Download all file into a folder named FFmpeg
on the your disk:
git clone git://git.videolan.org/ffmpeg.git ffmpeg
FFmpeg
needs several extra codecs you need some other source codes too. http://lame.sourceforge.net/download.php
http://xiph.org/downloads/
libogg
source , libvorbis
source and libtheora
source.git clone git://git.videolan.org/x264.git
git clone git://review.webmproject.org/
libvpx.githttp://www.xvid.org/Downloads.43.0.html
Xvid
source codeAll source code has been downloaded here.
Open the Terminal and copy paste every line below marked BOLD.
DISK_ID=$(hdid -nomount ram://26214400) && newfs_hfs -v tempdisk ${DISK_ID} && diskutil mount ${DISK_ID} && SOURCE="/Volumes/tempdisk/sw" && COMPILED="/Volumes/tempdisk/compile" && mkdir ${SOURCE} && mkdir ${COMPILED} && export PATH=${SOURCE}/bin:$PATH
cd ${COMPILED} || exit 1cd yasm-1.2.0./configure --prefix=${SOURCE} && make -j 4 && make install
cd ${COMPILED}cd libvpx./configure --prefix=${SOURCE} --disable-shared && make -j 4 && make install
cd ${COMPILED}cd lame-3.99./configure --prefix=${SOURCE} --disable-shared --enable-static && make -j 4 && make install
cd ${COMPILED}cd xvidcorecd build/generic./configure --prefix=${SOURCE} --disable-shared --enable-static --disable-assembly && make -j 4 && make installrm ${SOURCE}/lib/libxvidcore.4.dylib
cd ${COMPILED}cd x264./configure --prefix=${SOURCE} --disable-shared --enable-static && make -j 4 && make install && make install-lib-static
cd ${COMPILED}cd libogg-1.3.0./configure --prefix=${SOURCE} --disable-shared --enable-static && make -j 4 && make install
cd ${COMPILED}cd libvorbis-1.3.2./configure --prefix=${SOURCE} --with-ogg-libraries=${SOURCE}/lib --with-ogg-includes=/Volumes/tempdisk/sw/include/ --enable-static --disable-shared && make -j 4 && make install
cd ${COMPILED}cd libtheora-1.1.1./configure --prefix=${SOURCE} --with-ogg-libraries=${SOURCE}/lib --with-ogg-includes=${SOURCE}/include/ --with-vorbis-libraries=${SOURCE}/lib --with-vorbis-includes=${SOURCE}/include/ --enable-static --disable-shared && make -j 4 && make install
Compiling ZLIB
cd ${COMPILED}cd zlib./configure --prefix=${SOURCE} && make -j 4 && make installrm ${SOURCE}/lib/libz*dylibrm ${SOURCE}/lib/libz.so*
cd ${COMPILED}cd ffmpegexport LDFLAGS="-L${SOURCE}/lib"export CFLAGS="-I${SOURCE}/include"./configure --prefix=${SOURCE} --enable-gpl --enable-pthreads --disable-ffplay --disable-ffserver --enable-libvpx --disable-decoder=libvpx --enable-libmp3lame --enable-libtheora --enable-libvorbis --enable-libx264 --enable-libxvid --enable-avfilter --enable-filters --arch=x86 --enable-runtime-cpudetect && make -j 4 && make install
After doing all the hardcore compiling you are awarded a FFmpeg binary in the
sw/BIN
folder. Here is my binary.
$> mkdir frames$> ./ffmpeg -i input.mp4 -r 10 frames/frame%03d.png$> ./ffmpeg -t 25 -ss 00:00:01 -i loading.mp4 loading.gif
Copy the
ffmpeg
binary to/usr/local/bin
folder so that you can useffmpeg
command anywhere in your terminal directly.
ffmpeg -i loading.mp4 -s loading.gifffmpeg -t 25 -ss 00:00:01 -i loading.mp4 -s 540x960 loading.gif
-s:adjust gif size
ffmpeg -t 25 -ss 00:00:01 -r 15 -i loading1.mp4 -s 540x960 loading2.gif
-r:adjust frame rate
ffmpeg -i loading.mp4 -i logo.png -filter_complex 'overlay=10:main_h-overlay_h-10' loading1.mp4
Add a logo to the lower left corner
More ffmpeg param at FFmpeg Filters Documentation
ffmpeg -i input.mp4 -r 10 frames/frame%03d.png
i=0;dir=$(eval pwd); for name in $(ls $dir);do i=$(($i+1));p=$(eval printf "%03d" $i);mv $name frame$p.png; done
ffmpeg -r 10 -f image2 -i frame%03d.png -pix_fmt yuv420p loading.mp4
]]>1 | $ hexo new "My New Post" |
More info: Writing
1 | $ hexo server |
More info: Server
1 | $ hexo generate |
More info: Generating
1 | $ hexo deploy |
More info: Deployment
]]> 排序算法进阶(一)中介绍了快速排序算法
,但_它只适用于int类型的数组_,当我们实际使用中往往会设计到多种数据类型,如浮点类型、字符串类型,难道需要再为这些类型重写一个除了类型以外其他都一样的方法吗?
不用,java的泛型类型
给了我们这个便利。像我们平时经常用的List、Map、Vector,它的内部实现并不会对每一种数据类型进行适配,因为它们都使用了泛型。
关于泛型的介绍,这里就不罗嗦了,网上多的是,这里附一篇,读者自己研究吧。
为了将我们的排序方法改造为一个实用性更广的泛型方法,我们只需要在该方法的返回类型之前加一个<T>
,将方法里的int类型换成T泛型类型即可。
如QuickSort
方法:
改造前:
1 | public static void QuickSort(int n[], int left, int right) { |
改造后:
1 | public static <T> void QuickSort(T n[],int left,int right){ |
为了让T
类型之间进行比较,需要实现Comapable
接口,因此上述方法得改为:
1 | public static <T extends Comparable<T>> void QuickSort(T n[],int left,int right){ |
同样,Partion方法也类似改造:
1 | private static <T extends Comparable<T>> int Partion(T n[], int left, int right) { |
仅仅是稍微改改,这个方法就可以应用于String类型的数组的排序了 。
1 | String s[]={"7.0", "8.0", "1.0", "9.0", "5.0", "10.0", "3.0", "2.0", "11.0"}; |
然而,在实际使用过程中,并非总是那么顺心如意 。 虽然上面的String类型数组可以成功应用该排序方法,但之前传入的int[]数组在这时却不能用了 。
如上图,该方法不识别int类型了,怎么回事???
百思不得其解,只能网上去搜咯~
一开始还以为自己泛型用得不对,以为不是那么回事。
可是这篇文章里用得很好的,其中的display
方法可以传入int
、String
、float
类型参数,为什么我的不行呢?
回过头来自习对比一下两者的用法,不难看出我们这里多了一点:extends Comparable<T>
,会不会是它在搞鬼呢?
顿时间突然记起来我们平时使用List
、ArrayList
时,如果要创建整型数组,是不能用List<int>
的,必须用List<Integer>才行,同理List <float>
、List <double>
都必须用List<Float>、List<Double>。
为啥呢?
网上搜了一下“java中的int与Integer的区别”,果然发现有猫腻 ,见这里,讲得很详细,只恨自己java基础掌握得不牢啊!!!
还有一点就是,基本数据类型是不需要实现comparable
接口就可以直接进行比较的,而复杂类都实现了该接口,不信可以试一试,这里有个方法可以验证一个类是否实现了某个类接口,可以拿来判断一下Integer
、Double
、Float
、String
。(注:传递的接口名需要包括接口的全名,即包括包名的,这里应该是:java.lang.Comparable
,而不是Comparable
)
到了这里,终于搞明白了,扩展的排序方法在使用时,传入的参数只能是复杂类型,基本类型就没办法了咯~
]]>一:快速排序算法
二:堆排序算法
三:归并排序
四:二分查找算法
五:BFPRT(线性查找算法)
六:DFS(深度优先搜索)
七:BFS(广度优先搜索)
八:Dijkstra算法
九:动态规划算法
十:朴素贝叶斯分类算法
虽然前面自己整理里几个基本排序查找算法,但看了这篇文章真有点惭愧啊!因此想抽空学习整理一下这些算法。
首先就从快速排序算法入手,废话不多说,上图:
<img src=”https://lxzh.oss-cn-hangzhou.aliyuncs.com/Sorting_quicksort_anim.gif" width=”500”/ alt=”快速排序”>
不过,正式写之前还是有必要做一些简单介绍,掌握一个好的算法,它的背景尝试也得了解一下嘛!
快速排序(Quicksort
)是对冒泡排序的一种改进。由C. A. R. Hoare在1962年提出。
用分治法(Divide and conquer
)策略来把一个串行(list)分为两个子串(sub-lists):通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
① 以第一个关键字 n1 为控制字,将 [n1 ,n2 ,…,nk ] 分成两个子区,使左区所有关键字小于等于 n1 ,右区所有关键字大于等于 n1 ,最后控制字居两个子区中间的适当位置。在子区内数据尚处于无序状态。
② 把左区作为一个整体,用①的步骤进行处理,右区进行相同的处理。(即递归)
③ 重复第①、②步,直到左区处理完毕。
*1) *从数列中挑出一个元素,称为 “基准”(pivot),
*2) *重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
*3) * 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
1 | public static void QuickSort(int n[], int left, int right) { |
为了更加直观的显示算法运行过程中数组数据变化情况,这里仍旧加一个打印函数:
1 | public static void printList(int[] n) { |
随便写一个数组int n[] = { 7, 8, 1, 9, 5, 10, 3, 2, 11 };
通过打印中间结果来展示排序情况,完整代码如下:
1 | /** |
编译运行后输出结果:
1 | 7,8,1,9,5,10,3,2,11, |
仔细分析一下输出结果就能很好的理解该算法的原理啦!不多说了,读者自己去品味吧~ 如果闲来没事的话,读者可以利用这个排序原理自己制作文中的那个动图,有兴趣的可以尝试一下!
注:动态图片展示的是以数组最后一个值为基准,稍微修改一下上述代码就可以达到动图中的效果。
1 | using System; |
Extjs
;后台hibernate+struct
需求:前台查询结果分页显示,导出到excel
时需要导出所有符合查询条件的记录。
前台:
1 | var panel = Ext.getCmp('qRINFPanel'); |
qRINFPanel
为panel
的id
,params
里面还有其他参数,这个是存储前台查询用的参数对象。
前台将查询参数转换为json
格式放在url
中(不要用Extjs.encode
方法,很坑,后台request.getParameter
获取不到),后台拿到查询参数后,从数据库查询记录,然后生成excel
,以流的形式输出到前台下载即可。
后台代码:
1 | public void exportProjects() { |
其中,getProjects("","")
方法是从request
中取参数进行查询,返回List
数组
顺序查找算法
与二分查找算法
,其中二分查找算法又有两种实现方式,分别为递归查找与迭代查找。1 | int OrderFind(int* a,int n, int x) |
1 | public int OrderFind(int[] a, int x) |
1 | public int OrderFind(int[] a, int x) |
1 | //二分查找递归算法 |
1 | //二分查找迭代算法 |
1 | //二分查找递归算法 |
1 | //二分查找迭代算法 |
1 | //二分查找递归算法 |
1 | //二分查找迭代算法 |
冒泡排序
、插入排序
、对半插入排序
三种算法,并分别用C++
、C#
、Java
三种语言实现。1 | void BubbleSort(int* a,int n) |
1 | public void BubbleSort(int []array) |
1 | public void BubbleSort(List<Int32> array) |
1 | public void bubbleSort(int []array) |
1 | public void bubbleSort(List<Integer>array) |
1 | void InsertSort(int* a,int n) |
1 | public void InsertSort(int[] array) |
1 | public void InsertSort(int[] array) |
1 | public void InsertSort(List<Integer> array) |
1 | void HalfInsertSort(int* a,int n) |
1 | public void HalfInsertSort(int[] array) |
1 | public void HalfInsertSort(int[] array) |
1 | public void HalfInsertSort(List<Integer> array) |
对半插入排序
算法,它与对半查找算法
(二分查找算法
)有点相似之处。还是先上代码:
1 | public void HalfInsertSort(int[] array) |
在Program
的Main
函数里New一个(一)中写的那个CSharpArray
类,调用其中的方法测试:
1 | using System; |
输出结果:
排序前:
1 0 2 5 4 1 3 4
排序后:
0 1 1 2 3 4 4 5
为了显示for
循环中的每次执行结果,在HalfInsertSort
方法里加一句输出:
1 | public void HalfInsertSort(int[] array) |
再次编译运行即得以下结果:
排序前:
1 0 2 5 4 1 3 4
排序后:
0 1 2 5 4 1 3 40 1 2 5 4 1 3 40 1 2 5 4 1 3 40 1 2 4 5 1 3 40 1 1 2 4 5 3 40 1 1 2 3 4 5 40 1 1 2 3 4 4 50 1 1 2 3 4 4 5
从输出结果可以看出,遍历数组元素,利用对半查找原理找到其位置,插入,将受影响的元素一次前移。
同样,为了将以上的升序排序算法改为降序,也只需将while
括号里的大于号改为小于号即可。
这里,既然将其封装在一个类里,自然要提高其通用性,因此,我可以再给改排序算法添加一个参数,用来标记是升序排序还是降序排序。如:用“0”表示升序,用“1”表示降序,在不降低代码运行效率时以牺牲代码简洁性来修改一下代码:
1 | public void HalfInsertSort(int[] array, int orderType) |
再修改Main
函数里代码:
1 | using System; |
编译运行结果如下所示:
排序前:
2 4 0 3 5 6 1 1
升序排序后:
2 4 0 3 5 6 1 10 2 4 3 5 6 1 10 2 3 4 5 6 1 10 2 3 4 5 6 1 10 2 3 4 5 6 1 10 1 2 3 4 5 6 10 1 1 2 3 4 5 60 1 1 2 3 4 5 6
降序排序后:
1 0 1 2 3 4 5 61 1 0 2 3 4 5 62 1 1 0 3 4 5 63 2 1 1 0 4 5 64 3 2 1 1 0 5 65 4 3 2 1 1 0 66 5 4 3 2 1 1 06 5 4 3 2 1 1 0
最后,再次贴一下CSharpArray
类的代码:
1 | using System; |
OK,对半插入排序算法的基本演示就到此结束!
注:由于数组时代码里生成的随机数组,因此每次运行的结果基本不一样,可能与以上演示结果不同。
]]>1 | public void InsertSort(int[] array) |
同样在Program
的Main
函数里New一个(一)中写的那个CSharpArray
类,调用其中的方法测试:
1 | using System; |
输出结果:
排序前:
0 4 0 1 1 4 5 7
排序后:
0 0 1 1 4 4 5 7
为了显示for
循环中的每次执行结果,在InsertSort
方法里加一句输出:
1 | public void InsertSort(int[] array) |
再次编译运行即得以下结果:
排序前:
4 6 0 5 0 3 1 3
排序后:
4 6 0 5 0 3 1 30 4 6 5 0 3 1 30 4 5 6 0 3 1 30 0 4 5 6 3 1 30 0 3 4 5 6 1 30 0 1 3 4 5 6 30 0 1 3 3 4 5 60 0 1 3 3 4 5 6
由此可见,插入排序算法的基本思想是从左到右找出最小的一个往左移插入到合适的位置,遍历完整个数组之后,真个数组就变为有序啦!
同样,为了将以上的升序排序算法改为降序,也只需将while
括号里的大于号改为小于号即可。
这里,既然将其封装在一个类里,自然要提高其通用性,因此,我可以再给改排序算法添加一个参数,用来标记是升序排序还是降序排序。如:用“0”表示升序,用“1”表示降序,在不降低代码运行效率时以牺牲代码简洁性来修改一下代码:
1 | /// <summary> |
再修改Main
函数里代码:
1 | using System; |
编译运行结果如下所示:
排序前:
5 7 1 2 7 2 2 4
升序排序后:
5 7 1 2 7 2 2 41 5 7 2 7 2 2 41 2 5 7 7 2 2 41 2 5 7 7 2 2 41 2 2 5 7 7 2 41 2 2 2 5 7 7 41 2 2 2 4 5 7 71 2 2 2 4 5 7 7
降序排序后:
2 1 2 2 4 5 7 72 2 1 2 4 5 7 72 2 2 1 4 5 7 74 2 2 2 1 5 7 75 4 2 2 2 1 7 77 5 4 2 2 2 1 77 7 5 4 2 2 2 17 7 5 4 2 2 2 1
最后,再次贴一下CSharpArray
类的代码:
1 | using System; |
OK,插入排序算法的基本演示就到此结束!
注:由于数组时代码里生成的随机数组,因此每次运行的结果基本不一样,可能与以上演示结果不同。
]]>