如今安卓面试,对于算法的问题也愈来愈多了,要求也愈来愈多,特别是排序,基本必考题,并且还动不动就要手写,因此陆续要写算法的文章,也正好当本身学习。o(╥﹏╥)o面试
Android技能书系列:算法
Android基础知识数组
Android技能树 — Android存储路径及IO操做小结post
数据结构基础知识
算法基础知识
本文主要讲算法基础知识及排序算法。
咱们常常听到说XXX排序算法是稳定性算法,XXX排序算法是不稳定性算法,那稳定性究竟是啥呢?
举个最简单的例子:咱们知道冒泡排序中最重要的是二二进行比较,而后按照大小来换位置:
if(arr[j]>arr[j+1]){
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
复制代码
咱们能够看到这里的大小断定是前一个比后一个大,就换位置,若是相等就不会进入到if的执行代码中,因此咱们二个相同的数挨在一块儿,不会进行移动,因此冒泡排序是稳定的排序算法,可是若是咱们把上面的代码改动一下if里面的判断:
if(arr[j]>=arr[j+1]){
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
复制代码
咱们添加了一个等号,那这个时候就不是稳定排序算法了,由于咱们能够看到相等的时候它也换了位置了。
证实某个排序是不稳定很简单,好比你只要传入{2,2},只要换了位置就是不稳定,证实不稳定只要一种状况下是不稳定的,那么就是不稳定排序算法。
复杂度包括了时间复杂度和空间复杂度,可是一般咱们单纯说复杂度的时候都指时间复杂度。
用1+2+3+...+100为例: 普通写法:
int sum = 0, n = 100;//执行1次
for(int i =1 ; i <= 100 ; i++){ //执行n+1次
sum = sum + i; //执行n次
}
Log.v("demo","sum:"+sum); //执行1次
复制代码
咱们能够看到一共执行了2n+3次。(咱们这里是2*100+3)
高斯算法:
int sum = 0,n = 100; //执行1次
sum = (1+n)*n /2; //执行1次
Log.v("demo","sum:"+sum); //执行1次
复制代码
咱们能够看到一共执行了3次。
可是当咱们的n很大的时候,好比变成了1000,第一种算法就是2*1000+3,可是第二种仍是3次。
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化状况并肯定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记做:T(n) = O(f(n))。它表示随问题规模n的增大,算法执行时间的增加率和f(n)的增加率相同,称做算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。
咱们已经根据上面的解释看到了: T(n) = O(f(n));
因此第一种是:2*n + 3 = O(f(n)),第二种是3 = O(f(n));咱们能够看到是增加率和f(n)的增加率相同。
咱们之前学高数都知道:好比f(x) = x^3 + 2x ,随着x的变大,其实基本都是x^3的值,而2x的的值后面影响愈来愈小,因此有高阶的时候,其余低阶均可以随着x的变大而忽略,同理前面的相乘的系数也是同样,因此:
那咱们上面的第一种就变成了O(n)(ps:只保留最高位,系数变为1,),第二种变为了O(1)(ps:常数都变为1)
常见的时间复杂度:
最坏/最好/平均状况
引用《大话数据结构》中的例子,好比你要计算某一年是否是闰年,你能够写一个算法:
if(year%4==0 && year%100 != 0){
System.out.println("该年是闰年");
}else if(year % 400 == 0){
System.out.println("该年是闰年");
}else{
System.out.println("该年是平年");
}
复制代码
可是若是你在内存中存储了一个2150元素的数组,而后这个数组中是index是闰年的数组设置为1,其余设置为0,这样别人好比问你2000年是否是闰年,你直接查看该数组index为2000里面的值是否是1便可。这样经过一笔空间上的开销来换取了计算时间。
一个算法的优劣主要从算法的执行时间和所须要占用的存储空间两个方面衡量。
排序方法分为两大类: 一类是内部排序, 指的是待排序记录存放在计算机存储器中进行的排序过程;另外一类是外部排序, 指的是待排序记录的数量很大,以致于内存一次不能容纳所有记录,在排序过程当中尚需对外存进行访问的排序过程。
冒泡排序算法的运做以下:(从后往前) 1.比较相邻的元素。若是第一个比第二个大,就交换他们两个。 2.对每一对相邻元素做一样的工做,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。 3.针对全部的元素重复以上的步骤,除了最后一个。 4.持续每次对愈来愈少的元素重复上面的步骤,直到没有任何一对数字须要比较。
咱们以最简单的数组{3,2,1}来看:
咱们能够看到一种二个大的蓝色步骤(ps:3 - 1),而后每一个蓝色里面的交换步骤是一步步变少(ps:2,1)。
因此咱们就知道是二个for循环了:
/**
咱们的蓝色大框一共执行(3-1)次,
也就是(nums.length -1)次
*/
for (int i = length -1; i > 0; i--) {
/**
咱们蓝色大框交换步骤从(3-1)次开始,
且每一个蓝色大框里面的交换步骤在逐步减一,
正好就是上面的蓝色大框的(i变量)
*/
for (int j = 0; j < i; j++) {
//对比逻辑代码
}
}
复制代码
而后在里面加上判断,若是前一个比后一个大,交换位置便可。
public void bubbleSort(int[] nums) {
//传进来的数组只有0或者1个元素,则不须要排序
int length = nums.length;
if (length < 2) {
return;
}
for (int i = length -1; i > 0; i--) {
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
int data = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = data;
}
}
}
}
复制代码
咱们会先找一个关键数据,一般为第一个数,好比咱们这里的5,而后把数字小于5的数字都放在5的左边,大于5的数字都放在5右边,而后对于左边的数字使用相同的方法,取第一个为关键数据,对其排序,而后一直这么重复。
伪代码:
quickSort ( nums ){
//小于2个的数组直接返回,由于个数为0或者1的确定是有序数组
if(nums.length < 2){
return nums;
}
//取数组第一个数为参考值
data = nums[0];
//左边的数组
smallNums = (遍历nums中比data小的数)
//右边的数组
bigNums = (遍历nums中比data大的数)
//使用递归,对左边和右边的数组分别再使用咱们写的这个方法。
return quickSort(smallNums) + data + quickSort(bigNums);
}
复制代码
咱们一步步来看如何实现具体的代码:(我会先根据思路写一个步骤不少的写法,用于介绍,再写一个好的。)
其实要实现功能,这个很简单,咱们能够新建二个数组,而后再彻底遍历整个原始数组,把比参考值小的和大的分别放入二个数组。
//取第一个数为参考值
int data = nums[0];
//咱们先获取比参考值大的数及小的数各自是多少。
int smallSize = 0, bigSize = 0;
for (int i = 1; i < nums.length; i++) {
if (nums[i] <= data) {
smallSize++;
} else {
bigSize++;
}
}
//创建相应的数组,等会用来放左边和右边的数组
int[] smallNums = new int[smallSize];
int[] bigNums = new int[bigSize];
//遍历nums数组,把各自大于或者小于参考值的放入各自左边和右边的数组。
int smallIndex = 0;
int bigIndex = 0;
for (int i = 1; i < nums.length; i++) {
if (nums[i] > data) {
bigNums[bigIndex] = nums[i];
bigIndex++;
}else{
smallNums[smallIndex] = nums[i];
smallIndex++;
}
}
//左边和右边再各自使用递归调用
smallNums = quickSort(smallNums);
bigNums = quickSort(bigNums);
//而后再把smallNums的全部数值赋给data左边,而后nums中间为data,而后再bigNums把data右边。
for (int i = 0; i < smallNums.length; i++) {
nums[i] = smallNums[i];
}
nums[smallNums.length] = data;
for (int i = smallSize + 1; i < bigNums.length + smallSize + 1; i++) {
nums[i] = bigNums[i - smallSize - 1];
}
复制代码
固然这也是能够实现的,但是感受代码不少,并且每次调用quickSort进行递归的时候,都要新建二个数组,这样后面递归调用次数越多,新建的数组对象也会不少。咱们可不能够思路不变,参考值左边是小的值,参考值右边是大的值,可是不新建数组。答案是固然!!(这逼装的太累了,休息一下。)
剩下的左边和右边的数组也都经过递归执行这个方法便可。
public static void QuickSort(int[] nums, int start, int end) {
//若是start >= end了,说明数组就一个数了。不须要排序
if(start >= end){
return;
}
//取第一个数为参考值
int data = nums[start];
int i = start, j = end;
//当 i 和 j 尚未碰到一块儿时候,一直重复移动 j 和 i 等操做
while (i != j) {
//当 j 位置比参考值大的时候,继续往左边移动,直到找到一个比参考值小的数才停下
while (nums[j] >= data && i < j) {
j--;
}
//当 i 位置比参考值小的时候,继续往右边移动,直到找到一个比参考值大的数才停下
while (nums[i] <= data && i < j) {
i++;
}
//交换二边的数
if (i < j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
//当退出上面的循环,说明 i 和 j 的位置交汇了,更换参考值与 i 位置的值。
nums[start] = nums[i];
nums[i] = data;
//左边的数组经过递归继续调用,这时候由于参考值在 i 的位置,因此左边是从start 到 i -1
QuickSort(nums, start, i - 1);
//右边的数组经过递归继续调用,这时候由于参考值在 i 的位置,因此右边是从 i -1 到 end
QuickSort(nums, i + 1, end);
}
复制代码
能够看到,咱们是默认把第一个数字当作是排好序的数组(废话,一个数字固然是排好序的),而后每次后一个跟前面的进行比较排序,而后重复。因此咱们能够看到最外面的一共是N-1层。而后每一层里面的比较次数是跟当前层数数量相同。
好比第二层。咱们的数字2要和前面的1,3比较,那就要先跟3比较**(固然若是此处比3大就不须要比较了,由于前面已是个有序数组了,你比这个有序数组最大的值都大,前面的就不须要比了。)**若是比3小就要跟1比较,正比如较2次,跟层数相同。
因此基本代码确定是:
for(int i = 1; i < n ; i ++){
if( 当前待比较的数 >= 前面的有序数组最后一个数){
continue; //这就不必比较了。
}
for(int j = i-1 ; j >=0 ; j--){
// 当前待比较数 与 前面的有序数组中的数一个个进行比较。而后插在合适的位置。
}
}
复制代码
针对这个排序,代码原本只须要像下面这个同样便可:
public static void InsertSort(int[] nums) {
for (int i = 1; i < nums.length; i += 1) {
if (nums[i - 1] <= nums[i]) {
continue;
}
int va = i;
int data = nums[i];
for (int j = i - 1; j >= 0; j--) {
if (nums[j] > data) {
va = j;
nums[j + 1] = nums[j];
}
}
nums[va] = data;
}
}
复制代码
由于咱们这里的数字是连续的,因此间隔是1,可是为了下一个排序的讲解方便,咱们假设它们的间隔是可能不是1,因此改形成下面这个:
public static void InsertSort(int[] nums, int gap) {
for (int i = gap; i < nums.length; i += gap) {
if (nums[i - gap] > nums[i]) {
int va = i;
int data = nums[i];
for (int j = i - gap; j >= 0; j -= gap) {
if (nums[j] > data) {
va = j;
nums[j + gap] = nums[j];
}
}
nums[va] = data;
}
}
}
复制代码
希尔排序是直接插入排序算法的一种更高效的改进版本。
咱们假设如今是1-6个数字,咱们取数组的<数量/2>为间隔数(ps:因此为6/2 = 3),而后按照这个间隔数分别分组:
这样咱们能够当场有三组数组{3,4,},{1,6},{5,2} 而后对每组数组使用直接插入排序。而后咱们把间隔数再除以2(PS:为 3/2 = 1,取为1)。
而后再使用直接插入排序就能够获得最后的结果了。
因此还记不记得咱们上面的直接插入排序代码写成了public static void InsertSort(int[] nums, int gap)
,就是为了考虑上面的多个间隔不为1的数组。
因此只要考虑咱们的循环了几回,每次间隔数是多少就能够了:
public void ShellSort(int[] nums) {
int length = nums.length;
for (int gap = length / 2; gap > 0; gap /= 2) {
InsertSort(nums, gap);
}
}
复制代码
是否是发现超级简单。
(ps:这里记录一下,好比有10个数字,由于理论上是每次除以2,好比应该是5,2,1; 可是有些文章是写着5,3,1,有些文章写着5,2,1。我写的代码也是5,2,1。。。o(╥﹏╥)o到底哪一个更准确点。)
选择排序很简单,就是每次遍历,找出最小的一个放在前面(或者最大的一个放在后面),而后接着把剩下的再遍历一个个的找出来排序。
public void selectSort(int[] nums) {
int min;
int va;
for (int i = 0; i < nums.length; i++) {
min = nums[i];
va = i;
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] < min) {
min = nums[j];
va = j;
}
}
int temp = nums[i];
nums[i] = min;
nums[va] = temp;
}
}
复制代码
这里我暂时空着,由于跟二叉树有关系,因此我准备先写一篇二叉树的数据结构,而后再写这个排序。有兴趣的能够本身去搜下。
归并算法,指的是将两个顺序序列合并成一个顺序序列的方法。
好比有数列{6,202,100,301,38,8,1} 初始状态:6,202,100,301,38,8,1 第一次归并后:{6,202},{100,301},{8,38},{1},比较次数:3; 第二次归并后:{6,100,202,301},{1,8,38},比较次数:4; 第三次归并后:{1,6,8,38,100,202,301},比较次数:4;
这个引入网络上的图片了:
根据这个咱们能够看到,咱们要先不停的取中间拆分,左右二边拆开,一直拆到为一个元素的时候中止,而后再合并。
public void mergeSort(int[] nums, int L, int R) {
//若是只有一个元素,那就不用排序了
if (L == R) {
return;
} else {
int M = (R + L) / 2;//每次取中间值
mergeSort(nums, L, M);//经过递归再把左边的按照这个方式继续不停的左右拆分
mergeSort(nums, M + 1, R);//经过递归再把右边的按照这个方式继续不停的左右拆分
merge(nums, L, M + 1, R);//合并拆分的部分
}
}
复制代码
咱们继续经过图片来讲明上图最后合并的操做:
而后重复这个操做。若是好比左边的都比较完了,右边还剩好几个,只须要把右边剩下的所有都移入便可。
public void merge(int[] nums, int L, int M, int R) {
int[] leftNums = new int[M - L];
int[] rightNums = new int[R - M + 1];
for (int i = L; i < M; i++) {
leftNums[i - L] = nums[i];
}
for (int i = M; i <= R; i++) {
rightNums[i - M] = nums[i];
}
int i = 0;
int j = 0;
int k = L;
//左边尚未所有比较完,右边尚未所有比较完
while (i < leftNums.length && j < rightNums.length) {
if (leftNums[i] >= rightNums[j]) {
nums[k] = rightNums[j];
j++;
k++;
} else {
nums[k] = leftNums[i];
i++;
k++;
}
}
//二边的比完以后,若是左边还有剩下,就把左边的所有移入数组尾部
while (i < leftNums.length) {
nums[k] = leftNums[i];
i++;
k++;
}
//二边的比完以后,若是右边还有剩下,就把右边的所有移入数组尾部
while (j < rightNums.length) {
nums[k] = rightNums[j];
j++;
k++;
}
}
复制代码
先说明一个简单的桶排序吧:
好比咱们要给{5,3,5,2,8}排序,咱们初始化一组内容为0的数组(作为桶),只要把他们当作数组的index值,好比第一个是5,咱们就nums[5] ++ ; 这样咱们只要对这个数组遍历,取出里面的值,只要不为0就打印出来。
可是这样这里就会有一个问题了,就是若是个人数组里面最大的数是100000,那岂不是我初始化的数组长度是100000了,明显不能这样。咱们知道一个数字确定是由{0-9}这些数组成,只是处于不一样的位数而已,因此咱们能够仍是按照{0-9}来放入某个桶,可是是先按照个位数排序,而后按照十位数,百位数,千位数.....等来同样样来放。
因此咱们从左到右,从上到下的新数组的顺序是{1000,25,8,158}
因此新数组是{1000,8,25,158}
3.第三次,咱们比较百位数:
因此新数组仍是{1000,8,25,158}
这时候咱们就能够看到最终排序是{8,25,158,1000}
ps:若是还有万位数等,持续进行以上的动做直至最高位数为止
咱们既然知道了,要一直最外层循环要进行最高数的次数,因此咱们第一步是找出最大的数有几位:
//能够经过递归找最大值:
public static int findMax(int[] arrays, int L, int R) {
//若是该数组只有一个数,那么最大的就是该数组第一个值了
if (L == R) {
return arrays[L];
} else {
int a = arrays[L];
int b = findMax(arrays, L + 1, R);//找出总体的最大值
if (a > b) {
return a;
} else {
return b;
}
}
}
//也能够经过for循环找最大值:
public static int findMax(int[] arrays, int L, int R) {
int length = arrays.length;
int max = arrays[0];
for (int i = 1; i < length; i++) {
if (arrays[i] > max) {
max = arrays[i];
}
}
return max;
}
复制代码
而后主要的排序代码:
public static void radixSort(int[] arrays) {
int max = findMax(arrays, 0, arrays.length - 1);
//须要遍历的次数由数组最大值的位数来决定
for (int i = 1; max / i > 0; i = i * 10) {
int[][] buckets = new int[arrays.length][10];
//获取每一位数字(个、10、百、千位...分配到桶子里)
for (int j = 0; j < arrays.length; j++) {
int num = (arrays[j] / i) % 10;
//将其放入桶子里
buckets[j][num] = arrays[j];
}
//回收桶子里的元素
int k = 0;
//有10个桶子
for (int j = 0; j < 10; j++) {
//对每一个桶子里的元素进行回收
for (int l = 0; l < arrays.length; l++) {
//若是桶子里面有元素就回收(数据初始化会为0)
if (buckets[l][j] != 0) {
arrays[k++] = buckets[l][j];
}
}
}
}
}
复制代码
通常来讲外排序分为两个步骤:预处理和合并排序。首先,根据可用内存的大小,将外存上含有n个纪录的文件分红若干长度为t的子文件(或段);其次,利用内部排序的方法,对每一个子文件的t个纪录进行内部排序。这些通过排序的子文件(段)一般称为顺串(run),顺串生成后即将其写入外存。这样在外存上就获得了m个顺串(m=[n/t])。最后,对这些顺串进行归并,使顺串的长度逐渐增大,直到全部的待排序的记录成为一个顺串为止。
最后附上百度上的排序图:
文章哪里不对,帮忙指出,谢谢。。o( ̄︶ ̄)o
参考:
《大话数据结构》
《算法图解》
《啊哈,算法》