Android客户端面试基础(五)-数据结构与算法

来源:互联网 发布:全国房地产数据 编辑:程序博客网 时间:2024/06/04 18:33
  1. 链表与数组

    • 数组静态分配内存,链表动态分配内存;

    • 数组在内存中连续,链表不连续;

    • 数组元素在栈区,链表元素在堆区;

    • 数组利用下标定位,时间复杂度为O(1),链表定位元素时间复杂度O(n);

    • 数组插入或删除元素的时间复杂度O(n),链表的时间复杂度O(1)。

  2. 堆、栈、队列

    ① 堆是在程序运行时,而不是在程序编译时,申请某个大小的内存空间。即动态分配内存,对其访问和对一般内存的访问没有区别。

    ② 栈就是一个桶,后放进去的先拿出来,它下面本来有的东西要等它出来之后才能出来。(后进先出)

    ③ 队列只能在队头做删除操作,在队尾做插入操作.而栈只能在栈顶做插入和删除操作。(先进先出)

  3. 链表的删除、插入、反向

    链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

    链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

    typedef struct Node {int data;struct Node *next;    //next指针指向自己这种数据类型,这就形成了链表}SList;
  4. 字符串操作

    • 字符串查找

      String提供了两种查找字符串的方法,即indexOf与lastIndexOf方法。

      1、indexOf(String s)

       该方法用于返回参数字符串s在指定字符串中首次出现的索引位置,当调用字符串的indexOf()方法时,会从当前字符串的开始位置搜索s的位置;如果没有检索到字符串s,该方法返回-1
      String str ="We are students";int size = str.indexOf("a"); // 变量size的值是3

      2、 lastIndexOf(String str)

      该方法用于返回字符串最后一次出现的索引位置。当调用字符串的lastIndexOf()方法时,会从当前字符串的开始位置检索参数字符串str,并将最后一次出现str的索引位置返回。如果没有检索到字符串str,该方法返回-1.如果lastIndexOf方法中的参数是空字符串"" ,,则返回的结果与length方法的返回结果相同。
    • 获取指定索引位置的字符

      使用charAt()方法可将指定索引处的字符返回。

      String str = "hello word";char mychar =  str.charAt(5);  // mychar的结果是w
    • 获取子字符串

      通过String类的substring()方法可对字符串进行截取。这些方法的共同点就是都利用字符串的下标进行截取,且应明确字符串下标是从0开始的。在字符串中空格占用一个索引位置。

      1、substring(int beginIndex)

       该方法返回的是从指定的索引位置开始截取知道该字符串结尾的子串。
      String str = "Hello word";String substr = str.substring(3); //获取字符串,此时substr值为lo word

      2、substring(int beginIndex, int endIndex)

       beginIndex : 开始截取子字符串的索引位置 endIndex:子字符串在整个字符串中的结束位置
      String str = "Hello word";String substr = str.substring(0,3); //substr的值为hel
    • 去除空格

      trim()方法返回字符串的副本,忽略前导空格和尾部空格。

    • 字符串替换

      replace()方法可实现将指定的字符或字符串替换成新的字符或字符串

      oldChar:要替换的字符或字符串

      newChar:用于替换原来字符串的内容

      如果要替换的字符oldChar在字符串中重复出现多次,replace()方法会将所有oldChar全部替换成newChar。需要注意的是,要替换的字符oldChar的大小写要与原字符串中字符的大小写保持一致。

    • 判断字符串的开始与结尾

      startsWith()方法与endsWith()方法分别用于判断字符串是否以指定的内容开始或结束。这两个方法的返回值都为boolean类型。

      1、startsWith(String prefix)

      该方法用于判断当前字符串对象的前缀是否是参数指定的字符串。

      2、endsWith(String suffix)

      该方法用于判断当前字符串是否以给定的子字符串结束
    • 判断字符串是否相等

      1、equals(String otherstr)

       如果两个字符串具有相同的字符和长度,则使用equals()方法比较时,返回true。同时equals()方法比较时区分大小写。

      2、equalsIgnoreCase(String otherstr)

       equalsIgnoreCase()方法与equals()类型,不过在比较时忽略了大小写。
    • 按字典顺序比较两个字符串

      compareTo()方法为按字典顺序比较两个字符串,该比较基于字符串中各个字符的Unicode值,按字典顺序将此String对象表示的字符序列与参数字符串所表示的字符序列进行比较。

      如果按字典顺序此String对象位于参数字符串之前,则比较结果为一个负整数;如果按字典顺序此String对象位于参数字符串之后,则比较结果为一个正整数;如果这两个字符串相等,则结果为0.

    • 字母大小写转换

      字符串的toLowerCase()方法可将字符串中的所有字符从大写字母改写为小写字母,而tuUpperCase()方法可将字符串中的小写字母改写为大写字母。

      str.toLowerCase();

      str.toUpperCase();

    • 字符串分割

      使用split()方法可以使字符串按指定的分隔字符或字符串对内容进行分割,并将分割后的结果存放在字符数组中。

      1、str.split(String sign);

        sign为分割字符串的分割符,也可以使用正则表达式。  没有统一的对字符串进行分割的符号,如果想定义多个分割符,可使用符号“|”。例如,“,|=”表示分割符分别为“,”和“=”。

      2、str.split(String sign, in limit);

       该方法可根据给定的分割符对字符串进行拆分,并限定拆分的次数。
  5. Hash表的hash函数,冲突解决方法有哪些

    • 最常用的3个HASH函数:

      • 除法散列法:通过取k除以m的余数,来将关键字k映射到m个槽的某一个中去,即散列函数为:h(k) = k mod m

      • 乘法散列法:首先,用关键字k乘上常数A(0<A<1),并抽取kA的小数部分;然后,用m乘以这个值,再取结果的底(即整数部分)。散列函数可表达为:`h(k) = ⌊m(kA mod 1)⌋

      • 全域散列法(universal hashing)

    • 解决冲突常用的两种方法:

      • 链接法(chaining):把散列到同一槽中的所有元素都存放在一个链表中。每个槽中有一个指针,指向由所有散列到该槽的元素构成的链表的头。如果不存在这样的元素,则指针为空。如果链接法使用的是双向链表,那么删除操作的最坏情况运行时间与插入操作相同,都为O(1),而平均情况下一次成功的查找需要Θ(1+α)时间。α是装填因子。

      • 开放寻址法(openaddressing):所有的元素都存放在散列表中。因此,适用于动态集合大小n不大于散列表大小的情况,即装载因子不超过1。否则,可能发生散列表溢出。有三种技术常用来计算开放寻址法中的探测序列:线性探测、二次探测,以及双重探测。

  6. 各种排序
    这里写图片描述

    • 冒泡排序

      相邻的数据进行两两比较,小数放在前面,大数放在后面,这样一趟下来,最小的数就被排在了第一位,第二趟也是如此,如此类推,直到所有的数据排序完成。

    • 选择排序

      先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

    • 插入排序

      将数据分为两部分,有序部分与无序部分,一开始有序部分包含第1个元素,依次将无序的元素插入到有序部分,直到所有元素有序。插入排序又分为直接插入排序、二分插入排序、链表插入等,这里只讨论直接插入排序。它是稳定的排序算法,时间复杂度为O(n^2)。

    • 快速排序

      快速排序是目前在实践中非常高效的一种排序算法,它不是稳定的排序算法,平均时间复杂度为O(nlogn),最差情况下复杂度为O(n^2)。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

    • 归并排序

      归并排序具体工作原理如下(假设序列共有n个元素):

      • 将序列每相邻两个数字进行归并操作(merge),形成floor(n/2)个序列,排序后每个序列包含两个元素

      • 将上述序列再次归并,形成floor(n/4)个序列,每个序列包含四个元素

      • 重复步骤2,直到所有元素排序完毕

      归并排序是稳定的排序算法,其时间复杂度为O(nlogn),如果是使用链表的实现的话,空间复杂度可以达到O(1),但如果是使用数组来存储数据的话,在归并的过程中,需要临时空间来存储归并好的数据,所以空间复杂度为O(n)。

    • 堆排序

      • 先将初始数据R[1..n]建成一个最大堆,此堆为初始的无序区

      • 再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key

      • 由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。

      • 重复2、3步骤,直到无序区只有一个元素为止。

  7. 快排的partition函数与归并的Merge函数

    //p,r分别是这个要排序的区段的下标int partion(int *arElem, int p, int r){  int x = arElem[r];  int i = p,j = p;  for(; i < r; i++)  {    if(arElem[i] < x)    {      if(i != j)      {        exchange(arElem, i, j);      }      j++;    }  }  exchange(arElem, j, r);  return j;}
        //将有序数组a[first..mid] a[mid+1,last]合并    void merge(int a[],int first,int mid,int last,int tmp[]){        int i=first;//前一个数组的开始下标        int j=mid+1;//后一个数组的开始下标        int m=mid;//前一个数组的最后下标        int n=last;//后一个数组的最后下标        int k=0;//存放临时数组到tmp        while(i<=m&&j<=n){            if(a[i]<=a[j]){                tmp[k++]=a[i];                i++;            }else {                tmp[k++]=a[j];                j++;            }        }        while(i<=m){            tmp[k++]=a[i++];        }        while(j<=n){            tmp[k++]=a[j++];        }        //复制tmp到a数组        for(i=0;i<k;i++){            a[first+i]=tmp[i];        }    }
  8. 二分查找

    二分查找就是将查找的键和子数组的中间键作比较,如果被查找的键小于中间键,就在左子数组继续查找;如果大于中间键,就在右子数组中查找,否则中间键就是要找的元素。

  9. 二叉树

    树是一种比较重要的数据结构,尤其是二叉树。二叉树是一种特殊的树,在二叉树中每个节点最多有两个子节点,一般称为左子节点和右子节点(或左孩子和右孩子),并且二叉树的子树有左右之分,其次序不能任意颠倒。

    二叉树是递归定义的,因此,与二叉树有关的题目基本都可以用递归思想解决,当然有些题目非递归解法也应该掌握,如非递归遍历节点等等。
    这里写图片描述

    • 前根序遍历:先遍历根结点,然后遍历左子树,最后遍历右子树。

    ABDHECFG

    • 中根序遍历:先遍历左子树,然后遍历根结点,最后遍历右子树。

    HDBEAFCG

    • 后根序遍历:先遍历左子树,然后遍历右子树,最后遍历根节点。

    HDEBFGCA

  10. 图的BFS与DFS算法。

    • 深度优先遍历算法

      1、深度优先遍历的递归定义

        假设给定图G的初态是所有顶点均未曾访问过。在G中任选一顶点v为初始出发点(源点),则深度优先遍历可定义如下:首先访问出发点v,并将其标记为已访问过;然后依次从v出发搜索v的每个邻接点w。若w未曾访问过,则以w为新的出发点继续进行深度优先遍历,直至图中所有和源点v有路径相通的顶点(亦称为从源点可达的顶点)均已被访问为止。若此时图中仍有未访问的顶点,则另选一个尚未访问的顶点作为新的源点重复上述过程,直至图中所有顶点均已被访问为止。

        图的深度优先遍历类似于树的前序遍历。采用的搜索方法的特点是尽可能先对纵深方向进行搜索。这种搜索方法称为深度优先搜索(Depth-First Search)。相应地,用此方法遍历图就很自然地称之为图的深度优先遍历

      2、基本实现思想:

      (1)访问顶点v;

      (2)从v的未被访问的邻接点中选取一个顶点w,从w出发进行深度优先遍历;

      (3)重复上述两步,直至图中所有和v有路径相通的顶点都被访问到。

    • 广度优先遍历算法

      1、广度优先遍历定义

      图的广度优先遍历BFS算法是一个分层搜索的过程,和树的层序遍历算法类同,它也需要一个队列以保持遍历过的顶点顺序,以便按出队的顺序再去访问这些顶点的邻接顶点。

      2、基本实现思想

      (1)顶点v入队列。

      (2)当队列非空时则继续执行,否则算法结束。

      (3)出队列取得队头顶点v;访问顶点v并标记顶点v已被访问。

      (4)查找顶点v的第一个邻接顶点col。

      (5)若v的邻接顶点col未被访问过的,则col入队列。

      (6)继续查找顶点v的另一个新的邻接顶点col,转到步骤(5)。

      (7)直到顶点v的所有未被访问过的邻接点处理完。转到步骤(2)。

      广度优先遍历图是以顶点v为起始点,由近至远,依次访问和v有路径相通而且路径长度为1,2,……的顶点。为了使“先被访问顶点的邻接点”先于“后被访问顶点的邻接点”被访问,需设置队列存储访问的顶点。

  11. 最小生成树prim算法与最短路径Dijkstra算法

    • 普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex (graph theory)),且其所有边的权值之和亦为最小。

    • Dijkstra(迪杰斯特拉)算法是典型的最短路径路由算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止(BFS、prime算法都有类似思想)。Dijkstra算法能得出最短路径的最优解,但由于它遍历计算的节点很多,所以效率低。

  12. KMP算法

    KMP算法,是由Knuth,Morris,Pratt共同提出的模式匹配算法,其对于任何模式和目标序列,都可以在线性时间内完成匹配查找,而不会发生退化,是一个非常优秀的模式匹配算法。

  13. 大数据处理:类似10亿条数据找出最大的1000个数

    采用堆排序,具体做法是:

    构建一个只有10个元素的min-heap,那么根结点就是这10个数中最小的数,然后开始遍历数组,如果遇到的数比min-heap的根结点还小,直接跳过,遇到比min-heap根结点大的数,就替代根结点,然后对这个min-heap进行维护(也就是排序,保证heap的特征)。那么遍历完数组后,这个min-heap的10个元素就是最大的10个数。

  14. 动态规划、贪心算法、分治算法

    • 相同点:

      分治法和动态规划都是通过将问题分解成子问题,通过子问题的求解,实现对整个问题的求解。

    • 区别:

      • 子问题关系:

           分治法中划分出的子问题是完全相互独立的,子问题求解的之间无相互依赖关系,不相互影响。

           动态规划中划分出的子问题不是相互独立的,不同子问题通常包含一些公共子问题(公共子问题通常是子问题的组成部分),通过公共子问题的求解实现子问题的求解,子问题再通过对比和选择,求解出整个问题。

      • 共用程度:

           分治法划分出的子问题通常具有“相似”的结构,因此可以用相同的“算法思想”解决。由于只是思想相同,但结果并不通用,所以用自顶向下的递归过程来求解。

           动态规划分出的子问题通常具有“相同”的子问题,因此可以用相同的“中间结果”解决。在动态规划中通常会保留子问题的计算结果,供其他求解过程重用,而且为了提高重用程度,会使用自底向上的求解过程,而不直接使用相对费时的递归过程。

    • 使用场景(自我总结):

      • 分治法通常用于针对 不同的输入条件,按照要求进行加工。

      • 动态规划通常是针对 固定的输入条件,通过执行时的动态选择,求解得到最值。这和动态规划的“最优子结构”和“重叠子问题”特点有一定的关系。

      • 贪心算法可以看成是动态规划的一种减少执行过程中选择分支的近似算法。贪心算法在执行中求解最优时,利用贪心策略进行“直观或理论支持”的选择,而不是简单地max(所有选择),这可以减少选择分支的计算和判断,减小复杂度。代价就是有时候贪心算法得到的不是最优解,而是近似优解。

  15. 排列组合问题、回溯法

    • 排列组合

      排列组合通常用于在字符串或序列的排列和组合中,其特点是固定的解法和统一的代码风格。通常有两种方法:第一种是类似动态规划的分期摊还的方式,即保存中间结果,依次附上新元素,产生新的中间结果;第二种是递归法,通常是在递归函数里,使用for循环,遍历所有排列或组合的可能,然后在for循环语句内调用递归函数。

    • 回溯

      回溯算法也叫试探法,它是一种系统地搜索问题的解的方法。回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。用回溯算法解决问题的一般步骤为:

      1、定义一个解空间,它包含问题的解。

      2、利用适于搜索的方法组织解空间。

      3、利用深度优先法搜索解空间。

      4、利用限界函数避免移动到不可能产生解的子空间。

      问题的解空间通常是在搜索问题的解的过程中动态产生的,这是回溯算法的一个重要特性。