算法的简单概况

来源:互联网 发布:淘宝卖家上传视频教程 编辑:程序博客网 时间:2024/05/21 10:14
1. 字典树(单词搜索树)
      Trie是个简单但实用的数据结构,通常用于实现字典查询。我们做即时响应用户输入的AJAX搜索框时,就是Trie开始。本质上,Trie是一颗存 储多个字符串的树。相邻节点间的边代表一个字符,这样树的每条分支代表一则子串,而树的叶节点则代表完整的字符串。和普通树不同的地方是,相同的字符串前 缀共享同一条分支。还是例子最清楚。给出一组单词,inn, int, at, age, adv, ant, 我们可以得到下面的Trie:

可以看出:

  • 每条边对应一个字母。
  • 每个节点对应一项前缀。叶节点对应最长前缀,即单词本身。
  • 单词inn与单词int有共同的前缀“in”, 因此他们共享左边的一条分支,root->i->in。同理,ate, age, adv, 和ant共享前缀"a",所以他们共享从根节点到节点"a"的边。
  • 查询非常简单。比如要查找int,顺着路径i -> in -> int就找到了。
  • 搭建Trie的基本算法也很简单,无非是逐一把每则单词的每个字母插入Trie。插入前先看前缀是否存在。如果存在,就共享,否则创建对应的节点和边。比如要插入单词add,就有下面几步:
  1. 考察前缀"a",发现边a已经存在。于是顺着边a走到节点a。
  2. 考察剩下的字符串"dd"的前缀"d",发现从节点a出发,已经有边d存在。于是顺着边d走到节点ad
  3. 考察最后一个字符"d",这下从节点ad出发没有边d了,于是创建节点ad的子节点add,并把边ad->add标记为d。

Trie树属于树形结构,查询效率比红黑树和哈希表都要快。假设有这么一种应用场景:有若干个英文单词,需要快速查找某个单词是否存在于字典中。使用Trie时先从根节点开始查找,直至匹配到给出字符串的最后一个节点。在建立字典树结构时,预先把带有相同前缀的单词合并在同一节点,直至两个单词的某一个字母不同,则再从发生差异的节点中分叉一个子节点。

节点结构:
每个节点对应一个最大可储存字符数组。假设字典只存26个小写英文字母,那么每个节点下应该有一个长度为26的数组。换言说,可存的元素类型越多,单个节点占用内存越大。如果用字典树储存汉字,那么每个节点必须为数千个常用汉字开辟一个数组作为储存空间,占用的内存实在不是一个数量级。不过Trie树就是一种用空间换时间的数据结构,鱼和熊掌往往不可兼得。


Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。    Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

它有3个基本性质:

  1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
  2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
  3. 每个节点的所有子节点包含的字符都不相同。
题目:给你100000个长度不超过10的单词。对于每一个单词,我们要判断他出没出现过,如果出现了,求第一次出现在第几个位置。
    分析:这题当然可以用hash来解决,但是本文重点介绍的是trie树,因为在某些方面它的用途更大。比如说对于某一个单词,我们要询问它的前缀是否出现过。这样hash就不好搞了,而用trie还是很简单。
    现在回到例子中,如果我们用最傻的方法,对于每一个单词,我们都要去查找它前面的单词中是否有它。那么这个算法的复杂度就是O(n^2)。显然对于100000的范围难以接受。现在我们换个思路想。假设我要查询的单词是abcd,那么在他前面的单词中,以b,c,d,f之类开头的我显然不必考虑。而只要找以a开头的中是否存在abcd就可以了。同样的,在以a开头中的单词中,我们只要考虑以b作为第二个字母的,一次次缩小范围和提高针对性,这样一个树的模型就渐渐清晰了。


注:效率比哈希高  时间复杂度为线性
  

【1】查找概论

查找表是由同一类型是数据元素(或记录)构成的集合。

关键字是数据元素中某个数据项的值,又称为键值。

若此关键字可以唯一标识一个记录,则称此关键字为主关键字。

查找就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。

查找分为两类:静态查找表和动态查找表。

静态查找表:只作查找操作的查找表。主要操作:

(1)查询某个“特定的”数据元素是否在查找表中。

(2)检索某个“特定的”数据元素和各种属性。

动态查找表:在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经已经存在的某个数据元素。 主要操作:

(1)查找时插入数据元素。

(2)查找时删除数据元素。

好吧!两者的区别: 静态查找表只负责查找任务,返回查找结果。

而动态查找表不仅仅负责查找,而且当它发现查找不存在时会在表中插入元素(那也就意味着第二次肯定可以查找成功)


顺序表 说的是存储的数据在逻辑和内存中是按顺序存放的 不代表整个数据是有序的 比如使用顺序表存储 1,2,5,3,67,23 这个是顺序表 但不是有序顺序表

【2】顺序表查找

顺序表查找又称为线性查找,是最基本的查找技术。 它的查找思路是:

逐个遍历记录,用记录的关键字和给定的值比较:

若相等,则查找成功,找到所查记录; 反之,则查找不成功。

顺序表查找算法代码如下:

对于这种查找算法,查找成功最好就是第一个位置找到,时间复杂度为O(1)。

最坏情况是最后一个位置才找到,需要n次比较,时间复杂度为O(n) 显然,n越大,效率越低下。

【3】有序表查找

所谓有序表,是指线性表的数据有序排列。

(1)折半查找

关于这个算法不做赘述,代码如下:

<span style="font-size:18px;">#include <iostream>using namespace std;// 折半查找算法(二分查找) int Binary_Search(int* a,int n,int key){    int low = 1, high = n, mid = 0;  // 初始化    while (low <= high)    // 注意理解这里还有等于条件    {        mid = (low + high)/2; // 折半        if (key < a[mid])            high = mid -1;    // 最高小标调整到中位小一位        else if (key > a[mid])            low = mid + 1;    // 最低下标调整到中位大一位        else            return mid;         // 相等说明即是    }    return 0;}void  main (){    int a[11] = {0,9,23,45,65,88,90,96,100,124,210};    int n = Binary_Search(a,10, 9);    if (n != 0)        cout << "Yes:" << n << endl;    else        cout << "No:" << endl;}</span>


折半查找算法的时间复杂度为O(logn)。

(2)插值查找

考虑一个问题:为什么是折半?而不是折四分之一或者更多呢? 好吧,且看分解:

大家记不记得斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…….(从第三个数开始,后边每一个数都是前两个数的和)

然后我们会发现,随着斐波那契数列的递增,前后两个数的比值会越来越接近0.618,利用这个特性,我们就可以将黄金比例运用到查找技术中。

 

大家请看:

1

4. 通过上面知道:数组a现在的元素个数为F[k]-1个,即数组长为F[k]-1。

 mid把数组分成了左右两部分,左边的长度为:F[k-1]-1

 那么右边的长度就为(数组长-左边的长度-1): (F[k]-1)-(F[k-1]-1)= F[k]-F[k-1]-1 = F[k-2] - 1

5. 斐波那契查找的核心是:

a: 当key == a[mid]时,查找成功;

b: 当key<a[mid]时,新的查找范围是第low个到第mid-1个,此时范围个数为F[k-1] - 1个,

 即数组左边的长度,所以要在[low, F[k - 1] - 1]范围内查找;

c: 当key>a[mid]时,新的查找范围是第mid+1个到第high个,此时范围个数为F[k-2] - 1个,

 即数组右边的长度,所以要在[F[k - 2] - 1]范围内查找。

关于斐波那契查找, 如果要查找的记录在右侧,则左侧的数据都不用再判断了,不断反复进行下去。

对处于中间的大部分数据,其工作效率要高一些。

所以尽管斐波那契查找的时间复杂度也为O(logn),但就平均性能来说,斐波那契查找要优于折半查找。

可惜如果是最坏的情况,比如这里key=1,那么始终都处于左侧在查找,则查找效率低于折半查找。   

还有关键一点:折半查找是进行加法与除法运算的(mid=(low+high)/2)

插值查找则进行更复杂的四则运算(mid = low + (high - low) * ((key - a[low]) / (a[high] - a[low])))

而斐波那契查找只进行最简单的加减法运算(mid = low + F[k-1]-1)

在海量数据的查找过程中,这种细微的差别可能会影响最终的效率。


与二分查找相比,斐波那契查找算法的明显优点在于它只涉及加法和减法算,而不用除法。因为除法比加减法要占去更多的机时,因此,斐波那契查找的平均性能要比折半查找好。 
<span style="font-size:18px;">#include <iostream>#include <assert.h>using namespace std;#define  MAXSIZE  11// 斐波那契非递归void Fibonacci(int *f){    f[0] = 0;    f[1] = 1;         for (int i = 2; i < MAXSIZE; ++i)    {        f[i] = f[i-1] + f[i-2];    }}// 斐波那契数列/*---------------------------------------------------------------------------------  |  0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  8  |  9  |  10  |  11  |  12  |  ----------------------------------------------------------------------------------  |     0  |  1  |  1  |  2  |  3  |  5  |  8  |  13 |  21 |  34 |  55  |  89  |  144 | -----------------------------------------------------------------------------------*/// 斐波那契数列查找int Fibonacci_Search(int *a, int n, int key){    int low = 1;  // 定义最低下标为记录首位    int high = n; // 定义最高下标为记录末位(一般输入的参数n必须是数组的个数减一)    int F[MAXSIZE];    Fibonacci(F); // 确定斐波那契数列    int k = 0, mid = 0;    // 查找n在斐波那契数列中的位置,为什么是F[k]-1,而不是F[k]?    while (n > F[k]-1)    {        k++;    }    // 将不满的数值补全    for (int i = n; i < F[k]-1; ++i)    {        a[i] = a[high];    }    // 查找过程    while (low <= high)    {        mid = low + F[k-1] - 1; // 为什么是当前分割的下标?        if (key < a[mid])  // 查找记录小于当前分割记录        {            high = mid - 1;            k = k - 1;     // 注意:思考这里为什么减一位?        }        else if (key > a[mid]) // 查找记录大于当前分割记录        {            low = mid + 1;            k = k - 2;  // 注意:思考这里为什么减两位?        }        else        {            return (mid <= high) ? mid : n;  // 若相等则说明mid即为查找到的位置; 若mid > n 说明是补全数值,返回n        }    }    return -1;}void main(){    int a[MAXSIZE] = {0,1,16,24,35,47,59,62,73,88,99};      int k = 0;      cout << "请输入要查找的数字:" << endl;      cin >> k;    int pos = Fibonacci_Search(a, MAXSIZE-1, k);      if (pos != -1)          cout << "在数组的第"<< pos+1 <<"个位置找到元素:" << k;     else          cout << "未在数组中找到元素:" << k; }</span>

首先要明确:如果一个有序表的元素个数为n,并且n正好是(某个斐波那契数 - 1),即n=F[k]-1时,才能用斐波那契查找法。 如果有序表的元素个n不等于(某个斐波那契数 - 1),即n≠F[k]-1,这时必须要将有序表的元素扩展到大于n的那个斐波那契数 - 1才行,这段代码:
for (int i = n; i < F[k] - 1; i++)
  {
  a[i] = a[high];
  }
便是这个作用。

下面回答
第一个问题:看完上面所述应该知道①是为什么了吧。 查找n在斐波那契数列中的位置,为什么是F[k] - 1,而不是F[k],是因为能否用斐波那契查找法是由F[k]-1决定的,而不是F[k]。如果暂时不理解,继续看下面。

第二个问题:a的长度其实很好估算,比如你定义了有10个元素的有序数组a[20],n=10,那么n就位于8和13,即F[6]和F[7]之间,所以k=7,此时数组a的元素个数要被扩充,为:F[7] - 1 = 12个; 再如你定义了一个b[20],且b有12个元素,即n=12,那么很好办了,n = F[7]-1 = 12, 用不着扩充了; 又或者n=8或9或11,则它一定会被扩充到12; 再如你举的例子,n=13,最后得出n位于13和21,即F[7]和F[8]之间,此时k=8,那么F[8]-1 = 20,数组a就要有20个元素了。 所以,n = x(13<=x<=20)时,最后都要被扩充到20;类推,如果n=25呢,则数组a的元素个数肯定要被扩充到 34 - 1 = 33个(25位于21和34,即F[8]和F[9]之间,此时k=9,F[9]-1 = 33),所以,n = x(21<=x<=33)时,最后都要被扩充到33。也就是说,最后数组的元素个数一定是(某个斐波那契数 - 1),这就是一开始说的n与F[k]-1的关系。

第三个问题:对于二分查找,分割是从mid=(low+high)/2开始;而对于斐波那契查找,分割是从mid = low + F[k-1] - 1开始的; 通过上面知道了,数组a现在的元素个数为F[k]-1个,即数组长为F[k]-1,mid把数组分成了左右两部分, 左边的长度为:F[k-1] - 1, 那么右边的长度就为(数组长-左边的长度-1), 即:(F[k]-1) - (F[k-1] - 1) = F[k] - F[k-1] - 1 = F[k-2] - 1。 
斐波那契查找的核心是:
  1)当key=a[mid]时,查找成功;
  2)当key   3)当key>a[mid]时,新的查找范围是第mid+1个到第high个,此时范围个数为F[k-2] - 1个,即数组右边的长度,所以要在[F[k - 2] - 1]范围内查找。


哈希表的简述:
1,在以前的顺序表查找时,要查找某个关键字的记录就是从表头开始,挨个的比较记录a[i]与key的值是不是相等,直到相等才算是查找成功,返回i
2,有序表的时候,我们可以利用a[i]与key的"<"或者">"来进行折半查找,直到相等时返回为i,最终我们找到了下标
3,此时,我们发现,为了查找的结果,之前的"比较"都是不可避免的,但这是不是真的有必要呢???能不能直接通过关键字key得到要查找的记录内存存储位置呢?
散列表查找(哈希表):是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key).查找时,根据这个确定
的对应关系找到给定值key的映射f(key),若查找集合中存在这个记录,则必定在f(key)的位置上

对应关系f:散列函数,又称为哈希(hash)函数,采用散列技术讲记录存储在一块连续的存储空间中,这块连续的存储空间称为散列表或者hash表.那么关键字对应
的记录存储位置我们称散列地址

但是:在理想的情况下,每一个关键字,通过散列函数计算出来的地址都是不一样的,可现实中,这只是一个理想.我们时常碰到两个关键字,值不等,但是通过散列函数后,
求得的值是一样的.这就是冲突.我们只能尽可能的减少冲突,但是这不可避免啊,,,,


方法1:
除留余数法:f(key) = key mod p(p <= m)


处理散列冲突的几种方法:

1,开放定址法:一旦发生冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将
  记录存入
  
  如上,对12进行取余之后,37跟25冲突,我们要解决冲突,那么就会是这样的向后移动
  我们把这种解决冲突的开放定址法称为线性探测法.
  从上面的例子中还可以看到,本来不是同义词却需要争夺一个地址的情况,我们称之为:堆积
  
  如上,13本该在1的位置,但是位置被24占用了,所以应该向后移动.
  很显然,堆积的出现需要使得我们不断处理冲突,无论是存入还是查找效率都会大大降低.
  考虑深一步,如果发生这样的情况,当最后一个key = 34,取余为10,跟之前的22冲突,可是22  后面没有空位置了,尽管可以不断的求余数后得到结果,但效率很差,因此我们可以这样用:
  
  我们可以将其放在前面:
  
2,再散列函数法:
  对于我们的散列表来说:我们事先准备多个散列函数.
  
3,链地址法:
  也就是说:我们换一换思路,为什么有冲突就要换地方呢????我们直接就在原地想办法不可以吗???于是  我们就有了链地址法
  
  该方法可能会造成很多冲突的散列函数来说,提供了绝不会找不到地址的保障,但是,同时这也带来了查找时,  需要遍历链表的性能损耗....
4,公共溢出区法
  处理的方法都比较简单,凡是冲突的多跟着我走
  
  但是,如果冲突太多的话,当然就不适合用这个表了,

2. 搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。
假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。),请你统计最热门的10个查询串,要求使用的内存不能超过1G。
     什么是哈希表? O(1)常数
     哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
    哈希遍历做法:
    1.  后序遍历一遍整棵树
    2.  对于遍历到每一个节点,都获取到左右子节点的哈希值,然后将其拼接重新计算出自身的哈希值,并返回给父亲节点。
    解决方案:
    1. 通过hash进行统计
    2. 使用堆排
   题目中说明了,虽然有一千万个Query,但是由于重复度比较高,因此事实上只有300万的Query,每个Query255Byte,因此我们可 以考虑把他们都放进内存中去,而现在只是需要一个合适的数据结构,在这里,Hash Table绝对是我们优先的选择,因为Hash Table的查询速度非常的快,几乎是O(1)的时间复杂度。
    那么,我们的算法就有了:维护一个Key为Query字串,Value为该Query出现次数的HashTable,每次读取一个Query,如果 该字串不在Table中,那么加入该字串,并且将Value值设为1;如果该字串在Table中,那么将该字串的计数加一即可。最终我们在O(N)的时间复杂度内完成了对该海量数据的处理。
 
3.二分查找
    递归方法
    
  1. int BinSearch(int Array[],int low,int high,int key/*要找的值*/)  
  2. {  
  3.     if (low<=high)  
  4.     {  
  5.         int mid = (low+high)/2;  
  6.         if(key == Array[mid])  
  7.             return mid;  
  8.         else if(key<Array[mid])  
  9.             return BinSearch(Array,low,mid-1,key);  
  10.         else if(key>Array[mid])  
  11.             return BinSearch(Array,mid+1,high,key);  
  12.     }  
  13.     else  
  14.         return -1;  
  15. }  

非递归方法
     
     
  1. int bsearch(int *arr,int t,int l,int r)
      {
         int m;
        while(l<=r)//要探测的部分至少有1个元素,l==r的时候仅有1个元素
         {
             m=(l+r)/2;
             if(t>arr[m])
                 l=m+1;
             else if(t<arr[m])
                r=m-1;
            else
                return m;//发现t后马上返回结果
        }
        return -1;
    }
二分查找最好的时间复杂度是O(1),最坏的是O(logn)
上边的代码有很大的问题
    1. 没有考虑边界值输入,及数组指针是否为空
    2.  mid=(left+right)/2,有很大的隐患。两者之和超过了所在类型的表示范围时,那么middle就不是一个正确的值
     改为 middle = left + (right - left) / 2;
   3. 当查找的数中所有的数都是相同时,返回的不是第一个元素的位置,而是中间元素的位置
     使用记录上次找到位置,然后right=middle-1就可以解决
   
  • int Bi_Search(int a[],int n,int b)//  
  • {//返回等于b的第一个  
  •     if(n==0)  
  •         return -1;  
  •     int low = 0;  
  •     int high = n-1;  
  •     int last = -1;//用last记录上一次满足条件的下标  
  •     while (low<=high)  
  •     {  
  •         int mid = low +(high-low)/2;  
  •         if (a[mid]==b)  
  •         {  
  •             last = mid;  
  •             high = mid -1;  
  •         }  
  •         else if(a[mid]>b)  
  •             high = mid -1;  
  •         else  
  •             low = mid +1;  
  •     }  
  •   
  •     return last;  
  •   
  • }  
4.查大于要查找的数中第一个数                     
  1. int Bi_Search1(int a[],int n,int b)//大于b的第一个数  
  2. {  
  3.     if(n<=0)  
  4.         return -1;  
  5.     int last = -1;  
  6.     int low = 0;  
  7.     int high = n-1;  
  8.     while (low<=high)  
  9.     {  
  10.         int mid = low +(high - low)/2;  
  11.         if(a[mid]>b)  
  12.         {  
  13.             last = mid;  
  14.             high = mid -1;  
  15.         }  
  16.         else if (a[mid]<=b)  
  17.         {  
  18.             low =mid +1;  
  19.         }  
  20.     } 
  21.     return last;  
  22. }  

4. 实现一个栈?
     1.  链表两个指针 
     2.  队列
     3.  数组

5.怎么判断算法是不是稳定的?
    判断两个关键字的相对位置,比如说是两个相等的数字,在排序结束后,后面的数字跑在前面的数字前面去了,那就是不稳定
6. 八大排序算法(http://blog.csdn.net/hguisu/article/details/7776068)
          直接插入排序:
                将第一个数据作为一个子序列,从第二个数据开始和子序列进行比较,进行插入,直至整个序列有序为止
                要点: 要给原记录设立哨兵,作为判断数组边界之用
                时间复杂度:   最好:顺序  O(n)     最坏: 逆序 O(n^2)   平均: n^2=(最好+最坏)/2
                是稳定的,如果数据相等,将后面的数据放在相等元素的后面,相等元素的前后顺序没有改变
          希尔排序:
                 叫缩小增量排序/插入排序更好理解,每趟排序设置一个步长,最开始的步长为d=n/2.第二趟为d/2,步长依次减半,直到步长为1时,所有数据将全部排好序
                 不稳定
                 时间复杂度: 最好:顺序时,不用进行移动 O(n)    最坏: 逆序,所有数据都需要移动 O(n^2)  平均: n^2=(最好+最坏)/2
          选择排序:        
                  在数组中,找到最小的数与第1个位置的数交换,然后在剩下的数中找到最小的数与第二个位置的数交换,依次交换。
                  不稳定
                  时间复杂度: 最好最坏都是O(n^2) ,因为所有数据都要进行比较。找到最小的
                  优化: 原来只能确定一个最小值,现在每次确定两个数,减少循环的次数,最多只需进行 n/2趟循环
          堆排序:
                  小顶堆: 任一非叶子结点的值均小于左右孩子结点的值
                  堆对应一棵完全二叉树
                  初始化时将 n 个数建成一颗顺序存储的完全二叉树(一维数组存储二叉树),然后调整它们的存储序,使之成为一个堆
               对 n 个元素初始建堆的过程:
                    将 n个数建成一颗顺序存储的完全二叉树,然后从 n/2个结点为根结点开始建堆(最后一个结点是第 n/2结点的子树), 依次以前面的结点为根结点进行建堆,直至根节点
               不稳定(肯定是不稳定的,后面相等的数字可能成为根结点,跑在最前面)
        
               堆顶元素输出后,对剩余n-1个数重新建堆的过程:
                   将堆顶元素输出后,剩下的 n-1个元素,将堆低的元素送入堆顶,堆被破坏(因为根节点不能满足堆的性质),将根结点与左右结点中较小的元素进行交换,若与左节点交换,则左子树不满足堆的性质,对左子树进行重新建堆,直到堆的所有子树满足堆的性质。
               时间复杂度:
           冒泡排序:          
                   相邻的两个数依次进行比较,让大的让下沉,小的往上浮。
                   要进行n-1趟,每趟进行n-i-1比较。
                   稳定
                   时间复杂度: 最好是 n(有加上flag,记录每次交换的位置,如果flag没有变,即只进行一趟比较),最坏是 n^2
                   优化: 
                        加pos记录最后一次交换的位置,因此 pos 以后的数都是排好序的,每次只需要排 pos位置以前的数即可
                        每趟找到最大值和最小值。是排序次数减少了一半
            快速排序:
                   基于二分思想,先找个基准,将比它大的数放在它的右边,比它小的数放在它的左边,然后再进行递归
                   不稳定的(从两个方向向中间走,可能后面的指针对应的值会被放在相同值的前面) 
                   时间复杂度: 最好 O(nlogn) 最坏  O(n^2) (逆序,顺序,这里和冒泡差不多了)
                          对 n 个数进行递归划分,将比基准小的放在左边,比基准大的放在右边,递归的次数是 logn,每次划分要对 n 个数进行处理。所以是 nlogn。
                   优化:  
            归并排序:
                   基于分治的思想;
                   过程:
                  划分:  将n 个元素分解成成相等的两半,依次对分解的序列进行分解,直到序列个数为1时。
                  递归:  将两半序列递归分别进行排序
                   合并:  将排好序的左右序列进行和并  
                    时间复杂度: 我们将数据分解当成二叉树来看,每个数要比较 logn 次才能排好序,而有 n 个数,所以是 nlogn。
                    空间复杂度: O(1)  原地进行
                    稳定的排序,因为有 if(a[i]<a[i+1])
            基数排序:  
                   属于分配式排序
                   分为顺序基数排序和链式基数排序
                   顺序:  分别取出所有数字的个位,存放在数组中,进行计数排序,将排好的结果按照顺序再取出十位,再对其进行计数排序,直至持续到最高位为止
                   链式:  先根据所有数值的个位,将所有数值分配到0-9编号的桶里,将这些桶里的数值通过链表从左至右链起来,接着按十位来分配,直至持续到最高位为止
            桶排序:
                    创建1-9的9个桶,从数值的最高位开始,按照所有数值最高位的值将其分配到9个桶中,然后分别对桶内数据进行快排,最后将所有的桶内的数据链起来             
            计数排序:      
                   思想: 需要三个数组,分别是待排序数组,count数组,排好序数组,先统计每个数据出现,然后计算,每个数据小于等于它的数的个数(前一个数值个数加上自身数的个数),然后遍历待排序数组,从count数组中取出下标对应的值,以该值-1为下标,count 数组下标为值存在排好序数组,并将原来 count 数组的值-1,防止数组值相同
               
kmp算法
        思想:   将主串与子串进行匹配返回第一次匹配上的首位置,先求出子串各个序列的真前子串与真后子串,并求出 netx 数组,然后对主串与子串进行比较,当子串首个字符与主串的字符进行比较,不相等就与主串后一个字符相比较,当找到与第一个字符相等的字符,比较子串第二个字符,如果不相等就查前一个字符的 netx 数组的值,返回到响应下标对应的子串字符开始继续进行比较,重复这个过程,直到子串全部匹配,最后一个字符匹配后记录当前位置,该位置减去子串长度就是首位置下标,返回,如果没有找到返回-1.          

最短路径有哪些方法,地杰斯塔啦的过程及思想
           Dijkstra 算法,Floyd (弗洛德)算法
     Dijkstra思想:
            Dijkstra是从无向图中求一个顶点到所有顶点的最短路径,以起始点为中心向外层层层扩展,直到扩展到终点为止,属于贪心算法(每一步求的都是最优解),本质思想:按路径长度递增依次产生最短路径
            过程: 有两个集合,一个是未找到最短路径的顶点集合,另一个是找到最短路径顶点集合,再就是起始顶点到所有顶点的距离(可以直接达到就会有权值,不能到达就是无穷,自身是0),按照权值大小,每次找到最小的一个权值,将其顶点v0加入已找到的顶点集合,再以vo 为中心对可以到达的顶点的权值进行更新(如果加入的到v1的权值,小于原来的权值,就将其替换,否则,不变),并写出该路径,重复这个步骤,直到未找到最短路径的顶点全部加入了找到最短路径集合。
     Floyd 算法:
            Floyd算法是从有向图中,找到每对顶点的最短距离,属于动态规划(与分治法类似,是将问题分解成若干个子问题,但这些子问题不是独立的,在后续的子问题解决过程中,要用到已解决的问题的答案,为了避免重复结算,所以 用一个表将所有已解决的子问题的答案)
            从一个节点到另一个节点的最短路径,有两种走法,一种是直接达到,一种是经过若干节点到达,如果那种走法走出来的路径小,用哪个路径
          过程: 有两个矩阵,一个存的点到点的权值(直接直达或经过节点到达写权值,达到不了写无穷,到达自身写0),一个是路径path
,每次加入一个节点,通过该节点是否可以达到对应的顶点,分别对两个矩阵进行更新,重复这个步骤,就可以得到结果                  

N个数,求最小的K个数
       使用堆排,如果 N为10亿,可以采用分布式处理+堆排,将10亿数据给每个主机分配2.5亿数据,再分别对每个主机的数据进行堆排(在内存中建立大小为k 的大顶堆,每次拿出一个数与堆顶进行比较,比它小的数进行重新建堆,时间复杂度为 nlogk),找出最小K个数,然后对这4k个数据进行堆排,找出最小的K个数
大数相加
       思想:先对数组进行初始化,全部初始化为0,然后将两个字符串分别转成数组M,N,对字符串进行从后向前进行遍历,将最后字符串存在数组的0位置,因为计算是从个位开始的,将两个数组的值进行相加的和加上进位的值(如果没有进位,就是0)对10求余及对10求商,余存在一个新的数组A 中,商是进位,重复这个步骤计算即可

大数相减
      思想: 分为三种情况,如果m[i]+mc(减借位)>=n[i],直接相减就行,如果m[i]+mc(减借位)<n[i],则需要借位,则(m[i]+mc(减借位)+10)-n[i],mc设为-1.
大数相乘
        思想:  先对两个数组进行初始化,将字符串逆序存在数组里,进行二层循环,被乘数数组在外层,乘数在内层,对数组对应的值相乘的结果加上乘进位的值然后对10分别求余和求商,商为上一级的进位,然后对存结果的数组的值(A[i+j]+余数+进位)对10进行求商求余,余数存在数组中,商为乘进位,重复这个步骤即可
        for(i<B.length)
         {    mc=0,ac=0;  //mc 为乘进位  ac 为加进位
              for(j=0;j<A.length;j++)
               {
                    r1=(a[j]*b[i]+mac)%10;
                    mc=(a[j]*b[i]+mac)/10;
                    r2=(m[i+j]+r1+ac)%10;
                    ac=(m[i+j]+r1+ac)/10;
                    m[i+j]=r2;
               } 
          }
红黑树
   红黑树的特征:  也叫高级的二叉平衡树(从根节点到子节点的最长路径不超过最短路径的二倍,大致是平衡的),结点都有颜色,插入和删除结点时要遵循红黑规则
     红黑树的五个规则:
           1.  每个结点要么是红的,要么是黑的
           2.  根节点是黑的
           3.  每个叶结点是黑的(叶子结点)
           4.  如果一个结点是红的,那么它的俩儿子都是黑的
           5.  对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点 
     如果插入或删除时,没有满足该规则,为了保证插入,删除后的树依然维持为一颗新的红黑树,红黑树会进行自动修正
     动态修正方法:
       1.  改变结点颜色
       2.  旋转(分为左旋(将该结点与它的右孩子旋转,其他结点进行相应的调整)和右旋(将该结点与它的左孩子旋转,其他节点进行相应的调整))
     插入:  我们总是将插入的新结点涂为红色,如果涂成黑色那么每次新节点的插入都会导致新插入的节点那条所在路径需要修正以恢复红黑树的性质,相反,如果图成红色,当新插入节点的父节点是红色时,就破坏了性质(红节点的孩子都为黑节点),需要修正操作,或者根节点变为红色了(第一次插入或旋转导致),这种情况人很容易修复,只要将根节点再次涂黑就行
               也就是说,每次插入我们都要判断父节点是不是红色的,并且我们不确定是否修复会导致根节点改变或破坏根节点颜色(第一次插入为红色),所以每次修复后,再次将根节点涂成黑色
        有以下几种情况:
          1. 父节点为黑色,则子节点是红色不会影响
          2. 父节点为红色,则子节点是红色要进行相应的调整
          3.  叔父节点为红色时,不需要进行旋转操作,只需要将父节点与叔父节点涂为黑色,祖父节点涂为红色即可,因为祖父的父节点可能为红色,再依次向上进行调整即可
     删除: 
          1. 删除的结点没有子节点,直接删除
          2. 删除的结点有一个子节点,则可以直接删除,子节点替代它的位置
          3. 删除的结点有两个子节点,首先找到该结点的后继结点(中序遍历紧跟其后的结点,后继节点不会有左子结点),交换值,不交换颜色
     
二叉搜索(排序)树
   特殊的二叉树   
   特点:
       1.  所有非叶子结点至多有两个子节点
       2.  所有节点存储一个关键字
       3.  非叶子节的左孩子节点比它的节点小,右孩子节点比它的节点大    
     时间复杂度: 最坏O(n)(单支,相当于顺序查找)  最好O(logn)()
     有 n 个节点二叉树的最好是 O(n),最坏是O(logn)树高
二叉平衡树(AVL树)
     是特殊形式的二叉搜索树,即左右子树深度之差的绝对值不大于1(如果不满足性质,进行旋转操作(左旋和右旋))  
     出现的原因: 相对于二叉排序树来说层数比较少,查询效率比较高
     时间复杂度: O(logn)
B树
    是平衡的多路搜索树(不是二叉的)
     出现的原因: 节点可以存储多个数和多个分支,所以高度低,查询效率高(避免频繁的I/O操作,查找数据时,不需要多次I/O操作,一次可存很多数据)
     一次文件29查找的具体过程如下:
        
          1. 根据根结点指针找到文件目录的根磁盘块1,将其中的信息导入内存(磁盘I/O操作1次)
          2. 因为29>17,29<35,因此我们找到指针p2
          3. 根据指针p2,找到次磁盘3,将其中的信息导入内存(磁盘I/O操作2次)
          4. 依次重复这个步骤
    五个性质:
          1. 所有的叶子节点在同一层
          2. 每个节点最多有 m (m 为阶数)个孩子(m>=2)
          3. 根节点至少有两个孩子
          4. n 棵子树则有n-1个关键字
          5. 要查询的节点遍布整个树节点
B+树
     是B树的一种变形,
     1.  所有的结点都在叶子节点,并用链表从小到大连接起来,便于查找
     2.  n 棵子树有n-1个关键字
     3.  所有的非叶子节点都是索引节点
     为什么现在文件系统索引使用B+树而不是B树?
      1.  B树每次查找都要磁盘I/O操作,B+树将多个关键字放在一个盘块内,查找关键字时就很好找了,所以相对来说,I/O次数降低
      2.  B+树的要查找的节点都在叶子结点,只需要遍历链表即可  
     B+树与红黑树谁的效率高?
          B+树
B*树
     除了根节点的所有的节点增加了指向兄弟节点的指针
     当一个节点满时,如果它的下一个兄弟节点未满,那么将部分数据移到兄弟节点中,如果兄弟野蛮了,则增加新节点
     oracle的索引是基于B*树的
在几亿数据里,如何找到最大的qq号
          使用字典树,在遍历的过程中,建立字典树,只需要维持一颗字典树,分为两种情况,qq号等长与不等长,先说等长的情况,将后续qq号的首位与前一个进行比较,如果比它小,不需要做任何操作,比它大,就对字典树进行更新,如果相等,取qq号的第二个字符进行比较,依次比较,比它大就加入字典树,删除原来的结点,如果长度比它的大,就新建字典树。
     
在海量数据中,求重复次数最多的数
          哈希+桶排
        先将海量数据根据 hash函数,存放在若干的桶里(文件),桶的大小小于给予的内存,然后对桶内部用快排找出最大的,再对所有桶的最大进行比较,找出最大的值

1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。请怎么设计和实现?
            字典树
一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析
        这是一道分析时间复杂度的题,字典树+堆排,字典树时间复杂度 n*le(字符串的平均长度),堆排 nlogn,在两着中选较大的
       为什么字典树比hash快?
               因为字典树是通过前缀直接定位的,不需要哈希散列和冲突处理
二叉树怎么找到路径最长的两个节点
        找到根节点左子树的深度最深的叶子节点及右子树的深度最深的叶子节点,连起来路径是最长的
哈希算法
      先通过hash函数求出key对应的散列地址
      hash 函数的构造有以下几种:
       1. 直接定址法:f=a*key+b
       2. 数字分析法:  就是找规律,使得冲突降低(比如:出生年月日中,年月出现的冲突的概率比较大,月日出现冲突的概率比较小)
       3. 平方取中法:   对key平方然后进行取中间几位,适用于key位数较少
       4. 折叠法:  将key进行分割成几个部分,求这几个部分的叠加和作为散列地址(适用于位数较大)
       5. 除留余数法:  对一个值求余所得的余数为散列地址(对这个值的选取很重要,p<=m)
       6. 随机数法:  通过随机函数产生的值为散列地址
    如果两个key通过hash函数散列地址相同,则要解决冲突,有以下几种办法
        1. 开放地址法: 有冲突寻找下一个位置空间(
                                    1. 线性探测法:  挨着往后找,找到最后一个回到第一个
                                    2. 二次探索法:  1^2,-1^2,2^2,-2^2(双向找)
                                    3. 随机探测法:  由随机数产生进行探测
                                   )
        2.  再散列函数法: 多个散列函数f1,f2,f3...
                         当用一个函数发生冲突,换另一个函数计算
        3.   链地址法:  将相同的 key 的value,通过链表连接起来,找寻的时候,顺序访问链表
        4.   公共溢出区法:  查找时,先在基本表找,没找到再到溢出表中找(冲突少的时候,效率高)
      寻找的时候,再找再对key求散列地址,通过散列地址找value
hashMap
   hashMap就是对哈希表的封装,通过相应的get,put函数,通过链地址法解决冲突,不是线程安全的(实现没加互斥锁,考虑到线程同步)   一般是都是数组+链表
     和hash表不同的是:
      hashMap可以接收null 键值和值,hashMap不是线程安全的,hashTable是线程安全的
     为什么用它?
         用于快速查找时
     hashMap的工作原理是什么
           使用put(key,value)存储对象hashMap 中,使用get(key)从hashMap中获取对象,当我们调用put方法前,先调用hashCode()方法(hash函数)返回散列地址,根据散列地址对数组大小求余,将entry对象存在数组value中,所以获取对象就是map.entry
     当两个对象的hashCode相同会发生什么?
           会使用链表解决冲突,如果有冲突的对象,就将它们存在链表中(equal方法判断两个对象是否相等)
      如果两个键的hashCode相同,如何获取值对象?
             调用get()方法,会先调用hashCode()找到数组中位置,然后遍历链表区找寻对象
     “如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
         负载因子: hashMap 数据的容量占总数据容量的比  默认为0.75
         也就是说数组的75%被填满了,会将数组的大小调整为2倍的大小,并将原来的对象放入新的数组中(调用 hashCode重新定位)
      你了解重新调整HashMap大小存在什么问题吗?
           存在线程安全
链表排序
        



聚簇索引的双向链表是如何实现的
         双向链表的插入: 
                   将s节点插入到p节点前面去
          s->pre=p->pre;
          p->pre->next=s;
          s->next=p;
          p->pre=s;
        删除:
                    删除 p 节点
                   p->pre->next=p->next;
                   p->next->pre=p->pre;                         
                  free(p);
深度优先遍历
    属于图的遍历,使用栈
广度优先遍历
     属于图的遍历,使用队列
     由若干节点组成的一个有层次关系的数据结构,比如说二叉树,可以有0个,1个,2个子节点,最多有两个节点。
二叉树的遍历
     先序遍历: DLR   相当于深度优先遍历,
            递归实现:
                  如果结点指针不为NULL,先打印结点值,然后分别递归左子结点和右子节点
            非递归实现: 
                  当前节点进栈并打印,进入该结点的左子树,重复直到当前节点为空
                  如果栈不为空,则出栈,访问该结点的右子树
     中序遍历: LDR             
            递归实现:
                  如果结点指针不为NULL,先递归左节点,然后分别打印节点值和递归右子节点
            非递归实现: 
                  当前节点进栈,进入其左子树,重复直到当前节点为空
                  如果栈不为空,出栈,打印该节点,遍历该结点的右子树   
     后序遍历: LRD
            递归实现:
                  如果结点指针不为NULL,先递归左节点,再递归右子节点,最后打印节点值
            非递归实现: 
                  当前节点进栈,进入其左子树,重复直到当前节点为空
                  如果栈不为空, 判断栈顶元素的右子树是否为空,如不为空,则访问,为空栈顶节点出栈
     层次遍历: 从根节点开始按层遍历   相当于广度优先遍历  
          递归实现:  
               先求树的深度,然后使用循环,以根节点和深度作为参数,调用递归函数,递归函数里,深度小于1,就结束,深度等于1时,才可以打印,大于1,进行递归,分别对根节点的做孩子和右孩子进行递归。
          非递归实现:
               使用队列,遍历节点,然后将根结点加入队列,判断队列是否为空,不为空,将队列首个结点取出,分别将该结点的左右孩子结点加入队里,依次实现

递归与循环的区别:
          递归:
               方便了程序员,难为了机器
                 优点:  代码简洁,清晰,代码可读性好
                 缺点:  1.  递归是函数调用,函数调用是有时间和空间的消耗,每一次函数的调用,都需要在用户栈(我们编写的程序属于用户程序)中保存参数、函数的返回地址及临时变量,而且往栈里压数据是需要时间的,导致递归效率不高
                            2. 递归有很多重复的计算,递归的本质就是将一个问题分解成多个小问题,如果多个小问题存在相互重叠的部分,那就存在重复计算的部分
                            3.  栈溢出,如果函数调用的层级太多,,就会超出栈的容量(或者如果没加控制,将会造成栈溢出)
              递归的优化: 使用尾递归,尾递归和递归本质的区别是:递归入栈后再进行收缩,而尾递归占用恒量的内存
               尾递归就是一般递归必须等到下一级的函数调用返回后,本级函数才能得到结果,比如 f(n)依赖 f(n-1),尾递归不需要依赖,尾递归将要返回的数据放在参数里,直到遇到结束条件,一层一层返回,不需要栈操作,所以速度快也不会造成栈溢出
          循环
                  优点:  速度快
                  缺点:  代码可读性可能较差
         两者效率比较:
                   一般是循环快,因为不需要函数调用和入栈操作,但现在使用尾递归后,效率差不多
         两者的选用:
                    循环比较容易实现时,优先选循环,当很难建立循环方法时,使用递归(比如说:斐波那契数列用数列更简单,代码更简洁)
浮点数相加结果为什么有误差?
          因为二进制代码无法准确表示浮点数的十进制数据,十进制浮点数转成二进制会有精度缺失,float转成二进制后,整数和小数部分的二进制代码只能包含23位,即使double最多只能存储52位
         目前无法从根本解决这个问题,但是可以曲线解决这个问题
              先将小数乘以10或100转成整数,进行运算,然后除以10或100等获得结果
对一个空的对象,求sizeof,值为多少?
         一个空对象,不包含任何信息,本来应该是0,但是对象占用占用一定的内存空间,占多大内存由编译器决定,在 VC中,一个 c++对象值为1字节,在 xocde 中,OC 对象值为8(指针大小为16字节)
          如果在该类中添加一个构造函数和析构函数,求 sizeof,值为多少?
               还是1,调用这两个函数只需要知道地址就行,编译器不会因为有这两个函数在实例内添加额外的信息
          那如果把析构函数标记为虚函数呢?
                    
复制构造函数的参数不可为自身对象,因为实参对象传递给形参会调用复制构造函数,这样会造成死递归,而导致栈溢出

在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序,请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数
          因为二维数组中的数据排列是有规律的,所以将要查找的数与数组右上角的数进行比较,如果比右上角的数小,则去除该数所在的列,如果比右上角的数大,则去除该数所在列,如果相等,即找到
          测试用例:  数组中没有要查找的数,输入空指针,数组中有要查找的数
          (二维数组在内存中是顺序存储的)
字符串
        c/c++的所有字符串都是以’\0’结尾的,所以要注意,这块容易越界
        为了节省内存,字符串在内存的常量区,当多个指针指向用一个字符串时,该指针的地址是相同的,
一个算法要考虑哪些?
       时间效率
       是否会有内存覆盖
       解决问题的方案
       特殊的测试用例(是否为空指针等)   
替换空格
    请实现一个函数,把字符串中的每个空格替换成”%20”,例如输入”we are happy”,则输出”we%20are%20happy"
           空格的ASCII码是32,即十六进制的0x20,因此空格被替换为%20 
        解决方案: 时间复杂度O(n)
                  在原字符串的基础上替换,原字符串有足够的空间,先统计字符串中的空格个数,计算出替换之后字符串的长度,用到两个指针,第一个指针指向原字符串的尾部,第二个指针指向替换后字符串的首部,两个指针同时从后向前移动,直到第一个指针碰到空格,将空格替换成%20,第一个指针向前移动一格,第二个指针向后移动3格,重复这个步骤,直到第一个指针为 NULL。只需要一层循环,
         测试用例: 输入的字符串没有空格,字符串是个NULL指针,字符串是个空字符串,字符串中有多个连续的空格
 有两个排序的数组A1和A2,内存在A1的末尾有足够多的控件容纳A2, 请实现一个函数,把A2中的所有数字插入到A1中并且所有的数字是排序的            
               先计算 A2的大小,找到排序后的数组长度,需要三个指针,从后向前比较两个数组中的数字,大的插入到 A1的尾部,直到两个数组中的值都排完
          时间复杂度:  线性
          测试用例:  数组首地址是否为 NULL,数组是否为空,对相同的数字的处理
          解决方案:  从后向前比较两个数组,将大的数字放在尾部
          内存覆盖:  没有
输入一个链表的头结点,从尾到头打印出链表节点的每一个值
       首先要看能不能改变链表结构,如果可以的话,先对链表进行逆序,再打印,如果不可以,就使用栈和递归都可以
         栈: 遍历链表,将链表的所有节点入栈,再出栈打印
         递归:  递归本质就是栈结构,所以可以用栈,就可以使用递归,每次访问一个节点时,先递归它的后一个节点,待它递归返回后,再打印本节点
         时间复杂度: 线性
         测试用例: 链表首地址为NULL, 链表只有一个节点
          解决方案: 使用栈和递归
重建二叉树
     输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树,例如,前序遍历序列{1,2,4,7,3,5,6,8},中序遍历序列{4,7,2,1,5,3,8,6}
          先找前序遍历的第一个节点,该结点为根节点,通过根节点,将中序遍历的节点分为左右子树,然后递归构建
           时间复杂度: 
           测试用例:  二叉树是完全二叉树(倒数第二层必须是满的,最后一层,必须从左到右排列),满二叉树(没一层都必须是满的)
           解决方案:  每次寻找根节点+递归
用两个栈实现一个队列,并实现出队、入队函数
            栈1存放插入进来的数据,栈2负责出队,入队都从栈1入栈,出队,查看栈2是否为空,不为空就弹出栈顶元素,为空就将栈1的所有数据,逐个压入栈2.
            时间复杂度: 常数级 O(1)
            测试用例: 两个栈都为空出队,两个栈都满入队,
            解决方案: 两个栈
用两个队列实现一个栈
           先将数据加入队列1,出栈就队1的数据除了最后一个数,其他的都出队,加入队2,队1的数据出队,如果继续出栈,就将队2的数据除了最后一个,全部出队加入队1,队2的数据出队,再入栈数据时,看哪个队列中有数据,就在哪个队里中入队。
            时间复杂度:常数级 O(1)
            测试用例:  栈为空时出栈,栈满时入栈
            解决方案: 两个队列(也可以使用数组,链表)
旋转数组的最小数字  
      把一个数组最开始的若干元素搬到数组的末尾,称为数组的旋转,输入一个递增排序的数组的旋转,输出旋转数组的最小元素,例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1
            需要两个指针,第一指针指向数组首,第二指针指向数组尾,求p1与p2的中间元素,如果中间元素大于p1指向的元素,说明中间元素在前面的数组中, 将p1指向中间元素,然后再求p1,p2的中间元素,如果中间元素小于p2指向的元素,说明中间元素在后面数组中,p2指向中间元素,直到p1,p2相邻,指向相邻的元素,则p2就是最小的元素
             如果有0个元素旋转,怎么办?
                    在开始处判断,如果第一个元素小于最后一个元素,则第一个元素就是最小的元素
             如果p1,p2,中间元素指向的元素相同,怎么处理?
                     顺序查找 
             时间复杂度: 常数级 O(1),  顺序查找为线性 O(n)
             测试用例:  数组为空,数组首指针为NULL,旋转0个元素,p1,p2,p3指针指向的元素相同
             解决方案:  求p1,p2的中间元素,与p1或 p2进行比较,让 p1,p2指向中间元素,重复这个步骤,
 斐波那契数列
        写一个函数,输入n,求斐波那契数列的第n项   
             一般的递归效率太低了,重复计算太多了,所以使用循环可以解决
                扩展: 还可使用数学公式计算矩阵的乘方
                时间复杂度:  线性 O(n)
                测试用例: 功能测试
          一只青蛙一次可以跳上1级台阶,也可以跳2级台阶,求该青蛙跳上一个n级的台阶总共有多少跳法
                 当第一次跳一级,则有f(n-1)种跳法,当第一次跳两级,则有f(n-2)种跳法,则f(n)=f(n-1)+f(n-2)
          一只青蛙一次可以跳上1级台阶,也可以跳2级台阶,也可以跳n级台阶,求该青蛙跳上一个n级的台阶总共有多少跳法
             f(n)=f(n-1)+f(n-2)+f(n-3)+…f(1);
              而 f(n-1)=f(n-2)+f(n-3)+…f(1);
              所以 f(n)=2f(n-1)   所以由数学归纳法: f(n)=2^(n-1)
          我们可以用2*1的小矩形横着或竖着去覆盖更大的矩形,请问用8个2*1的小矩形五重复地覆盖一个2*8的大矩形。总共有多少方法? 
              覆盖时有两种方式要不横着放要不竖着放,如果横着放,下面得横着放个,右边是2*6矩形,竖着放右边有个2*7的矩形,因此 f(8)=f(7)+f(6),这是斐波那契数列 
二进制中1的个数
          数值与比它小1的数值相与,就会相当于去掉该值的二进制的最后一位1,那我只要统计循环次数就行了
           int  number(int n)
           {
                  int count=0;
                  while(n)
                   {
                         count++;
                         n=n&(n-1);
                   }   
           }
         时间复杂度:  线性 O(n)
         测试用例:  正数,负数,0.
         解决方案:  将数与该数减一的值求与
 用一条语句判断一个整数是不是2的整数次方
          如果一个整数是2的整数次方,则该数的二进制只有一位1,只需要将该值与该值-1的值相与,结果为0,就是
 输入两个整数m,n,计算需要改变m 的二进制中的多少位才能得到 n          
          将两个数进行异或运算,计算机计算结果中二进制1的位数
数值的整数次方
          比如说: a^n=a^n/2*a^n/2  n为偶数
                      a^n=a^n-1/2*a^n-1/2*a  n为奇数
          根据这个公式,递归求解即可,
         注意: 用右移运算符(expoent>>1)代替除以2,用与运算符(expoent&0x1==1)代替了求余,位运算效率比乘除的效率要高得多(位运算有底层的cpu指令支持)
                   底数不能为0,如果指数为-1,则0会成为分母(判断底数是不是0时,不能写base==0,在计算机内对浮点数的存储有误差,float只有23位,double 只有52位,所以求两个小数的差的绝对值小于10^-7)
                   如果指数为负数,则先对负值进行求指数求绝对值,计算完结果再求倒数
             测试用例: 底数和指数分别为正数,负数,0
打印1到最大的 n 位数 
          输入数字n,按顺序打印从1到最大的 n 位十进制数,比如输入3,则打印出1、2、3一直到最大的999
           不能盲目打印,要考虑大数问题
           
在 O(1)时间删除链表结点
       给定单向链表头指针和一个节点指针,定义一个函数在O(1)时间删除该结点,
               将该结点的下一个节点的内容全部复制到该节点中,然后将该结点的next的指针指向下下一个节点,就会被删除
               如果要删除的节点位是最后一个节点,怎么处理?        
                     只能顺序查找
               如果链表中只有一个节点,删除节点后,要将链表头指针置为NULL
              测试用例:链表头指针为 NULL,给的节点指针为NULL,链表只有一个节点,给的节点是最后一个节点
调整数组顺序是奇数位于偶数前面  
          使用两个指针,第一个指针指向第一个元素,第二个指针指向最后一个元素,如果p1指向的是偶数,则看p2,p2指向是奇数的话,则交换如果 p1指向的是奇数,则向后移动 p1,直到p1指向偶数,如何p2指向的是偶数的话,向前移动直到p2指向奇数,p2在p1的前面时,结束,表示已经排好
          扩展: 如果改为负数位于非负数的前面,怎么办? 如果能被3整除的在前面,不能被整除的在后面,怎么处理呢?
                 将原来的的奇偶数的判断条件,改为函数指针,通过函数指针返回值判断,每次只需要修改函数即可
          时间复杂度: 线性 O(n)
          测试用例: 数组手地址为 NULL,全部都是偶数或奇数,奇数都在偶数前面,偶数都在奇数前面,数组只有一个元素
链表中倒数第K个节点
          1. 使用栈
          2. 使用两个指针,第一个指针指向首地址,第二个节点指向第k个节点,两个指针同时向后移动,第二个指针指向NULL时,第一指针就是倒数第k个节点
          时间复杂度:O(n)
          测试用例:  首地址为 NULL,k 为负数,节点的个数小于 k,
求链表的中间节点
       如果节点个数为奇数,返回中间节点,如果节点个数为偶数,返回中间节点的任一个
       用两个指针,第一个指针一次走一步,第二指针一次走两步,当第二个指针指向链表末尾时,第一个指针刚好走向中间节点(奇偶数都满足)
判断一个单向链表是否形成一个环形结构
         我们用一个指针解决不了问题时,就用两个指针解决
反转链表             
        定义三个指针,分别指向当前的节点,当前节点的前一个和后一个节点,将该该结点的next指针指向前一个节点,保存后一个节点的地址
        时间复杂度:  O(n)
        测试用例:   首地址为NULL,只有一个节点,有多个节点
合并两个排序的链表
       用一个新的链表,分别将两个链表的节点进行比较,小的就加入新链表中,
       时间复杂度: O(n)
       测试用例: 链表的首地址为NULL,一个链表只有一个节点,节点的值相同
输入两颗二叉树A和B,判断B是不是A的子结构
          第一步:在A树中找和 B树根节点一样的节点
          第二步: 找到后,判断树 A中是否有和 B 树一样的结构
       测试用例: 两棵树的根节点为 NULL,二叉树的所有节点只有左子树或者右子树
二叉树的镜像:
       交换根节点的左右节点,只要遍历遇到节点就交换该节点的左右节点即可
       测试用例: 根节点为 NULL,只有一个节点,只有左节点或只有右节点
判断一个数组是不是某二叉搜索树的后续遍历
        在后续遍历的序列中,最后一个数字是根节点,根据根几点将数组氛围两部分,因为二叉搜索树,左边的节点都比右边的结点小,然后进行递归,左部分的最后一个数是做子树的根节点,继续分,在分的过程中,遇到不满足条件的就直接返回 false
       测试用例:根结点为NULL,只有一个节点,
二叉树中和为某一值的路径 
          前序遍历,遇到的每个节点都入栈,遍历到叶子结点,栈的值总和还是不能满足,就出栈,直到栈的值总和满足某一值
          可以用栈就可以用递归
          测试用例: 根节点为NULL,只有一条满足条件,有多条满足条件,没有符合的条件
复杂链表的复制
     每个结点除了有个 next 指针外,还有个指向任意结点的指针
           1.  在原始链表上复制每个节点,用next连起来
           2.  用哈希表存起来<C,C’>关系(A的mnext指向C,则 A’的mnext指向的就是C')
           3.  将长链表拆分成两个链表(将奇数的节点用 next 连接起来,将偶数的节点用 next 连接起来)
       测试用例:   链表的首地址为NULL,只有一个节点,mnext 指向自身
将二叉搜索树转换成双向链表,转成的双向链表是排好序的
       对二叉搜索树进行中序遍历,对根节点的左子树先进行连接(此时左子树已经是排好序的双向链表了),再对根节点的右子树再进行连接,再连接上根节点,使用递归
         测试用例:根结点为NULL, 只有一个节点,只有左子树或右子树
数组中出现次数超过数组长度一半的数字
       1.  对数组进行排序(快排),中间数一定是出现超过一半的数字
       2.  遍历数组时保存两个值,一个是数组中的数字,一个是次数,默认次数为1,当下一个数字与前一个数字相同时,次数+1,不相同时,如果次数为不为0,则次数减1,如果次数为0 ,则将数字替换为下一个数字,次数为1.重复这个步骤,最后对应的次数就是1的数
        时间复杂度: O(logn),O(n)
        测试用例:  数组首地址为NULL,数组中只有一个数,数组中的值全部相同,不存在超过一半数字       
连续子数组的最大和
     如输入数组为{1,-2,3,10,-4,7,2,-5},
         比如说第一个数字与第二数字的和小于0,将前面的和遗弃,从第三个数值开始,如果加上的值为负数,因为不知道后面的值是怎么样的,所以先将数值保存下来,有可能是最大的,只要和不小于0,就一直加,如果后面的值大于保存下来的值,就替换(只能保存一个最大值)
         时间复杂度: 线性 O(n)
         测试用例: 数组首地址为NULL,数组中只有一个元素,数组中全是证书或负数
第一个只出现一次的字符
      如输入”abaccdeff”,则输出’b'
             先使用哈希表,将遍历的字符的次数存在value中,第二次遍历时,首先找到次数为1的字符就是第一个出现一次的字符
             哈希表的大小为256,因为有256个字符
             时间复杂度:  O(n)
             测试用例:   字符串地址为NULL,字符串中只有一个字符,字符串中只存在一次的字符,字符串中不存在只出现一次的字符
 输入两个字符串,从第一个字符串删除第二个字符串中出现过的所有字符
       用哈希表将第二个字符串的所有的所有字符,当我们遍历字符串1时,只需要O(1)就可以判断是否在字符串2中,即可删除
       时间复杂度: O(n)
       测试用例: 字符串地址为NULL,字符串2与1中的字符相同,字符串2与1中的字符全不同
删除字符串中的所有重复出现的字符,如:输入”google”,结果为”gole"
      用哈希表,将遍历过的字符的 value设为1,后面的字符发现该字符 value 为1后,相同的字符删除
      时间复杂度: O(n)
      测试用例:字符串地址为NULL,没有重复的字符,全部重复,
在英语中,如果两个词中出现的字母相同,并且每个字母出现的次数相同,那么这两个单词互为变位词,例如 slient与 listen,判断两个英语单词是否为变位词   
          使用哈希,为扫描到的字符的次数+1,第二次扫描时,对对应的字符的次数-1,最后如果字符的次数全部为0,则互为变为词
          时间复杂度: O(n)
          测试用例:  字符串地址为NULL,两个单词完全相同,两个单词长度不一样长
数组的逆序对
     在数组中的两个数如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对,输入一个数组,求出这个数组中逆序对的总数
          

输入两个链表,找出它们的第一个公共节点
      两种方式:
        1.  使用栈,考虑到链表不一样长,使用栈很合适,每次从两个栈顶取出元素,直到取出的两个元素不一样,则前一个就是第一个公共节点 ,这种方式用空间换时间,时间复杂度为 O(n)
        2.  先遍历两个链表,统计链表中节点的个数,将较长的链表先走两个链表长度差的步数,然后两个节点依次进行比较,直到遇到节点相同,则找到,时间复杂度:O(n)   这种方式不需要辅助栈,提高了空间利用率,
 数字在排序数组中出现的次数
       输入排序数组{1,2,3,3,3,3,4,5}和3,由于这个数字出现了4次,输出4
            两种方式:
            1.  哈希  (空间换时间)
            2.  使用二分查找,因为数组是有序的,如果中间数大于k,则说明k在左子树,如果中间树小于k,说明 k 在右子树,如果相等,则判断中间数前一个数是不是与中间数相等,不相等说明中间数是第一个出现的数,相等说明,中间数不是第一个出现的数,则递归在左子树上找第一个数,右子树重复这个步骤(就是找第一个数和最后一个数)
           时间复杂度:  O(n)
           测试用例:  数组地址为 NULL,数组中不存在这个数,数组中只有一个数,数组中只有一个这个数
二叉树的深度
    输入一棵二叉树的根节点,从根节点到叶子节点依次经过的节点形成树的一条路径,最长的路径的长度为树的深度
         二叉树的深度为左右子树中深度较大的值+1,
         int treePath(treeNode *root)
         {
               if(root==NULL)
                    return 0;
               int left=treePath(root->left);
               int right=treePath(root->right);
               return (left>right)?(left+1):(right+1);
         }
   测试用例:根节点指针为 NULL,二叉树只有一个节点,只有左子树或右子树
判断一个二叉树是不是平衡二叉树
      使用后续遍历效率高,因为每个节点只被遍历一次,从下向上遍历
      一般的思路是:前序遍历到一个节点,求左右子树的深度,依次递归遍历节点,这种方式是从上向下遍历,遍历的节点不会重复
      测试用例: 根节点指针为NULL,该树是平衡二叉树,不是平衡二叉树,所有子树没有右子树
数组中只出现一次的数字  
       一个整数数组除了两个数字外,其他数字都出现了两次,请写出程序找出只出现一次的数字,要求时间复杂度为O(n),空间复杂度为 O(1)
       时间复杂度: 运行算法所需要的时间  空间复杂度: 算法所需要的空间的大小 ,O(1)表示所占的空间不变
       每个数字对自身求异或为0,基于这个思路,
         1.  我们对这个数组内的所有数据相异或,因为是两个两个出现的,所以最后剩下两个只出现一次的数,最后得到的值比如说为0010
         2. 说明最后两个数在第二位上的值不一样,根据第二位值,将数组的值分为两部分,再分别对各部分的值进行异或,最后各部分剩下的数就是只出现一次的数

翻转单词顺序
     输入一个英文句子,翻转句子中单词的顺序,但单词内容顺序不会变,例如输入:”I am a student.”,则输出”student.  a  am  I"
          思路: 先翻转句子,再翻转句子中的单词
          实现一个函数专门用于翻转字符串,先调用函数翻转整个句子,参数为句子的开始地址和结束地址,然后
          pbegin=pend=pdata;
          while(pend)
          {
               if(*pbegin==‘ ')
               {
                    pbegin++;
                    pend++;
               }
              else  if(*pend==‘ ‘||*pend==‘\0')
               {
                    rever(pbegin,—pend);
                    pbegin=++pend;
               }
               else{
                     pend++;
               }
          }
       测试用例: 字符串指针为NULL,字符串为空,句子中只有一个单词,句子中有多个单词
字符串的左旋转操作是字符串前面的若干字符转移到字符串的尾部,输入字符串”abcdefg”和数字2,该函数将返回左旋2位得到结果”cdefgab"
          也是通过翻转操作,先将数组分为两部分,一部分是ab,另一部分是cdefg,先对单词内部进行翻转,然后对整个字符串进行翻转
          测试用例: 字符串指针为 NULL,字符串为空串,给出的数字越界(访问的内存不属于字符串),
约瑟夫环
       0,1,….n-1这 n 个数排列成一个圆圈,从数字0开始每次从这个圆圈里删除第 m 个数字,求出这个圆圈里剩下的最后一个数字 
         1.  使用循环链表,走m-1步删除一个结点(这种方式会造成重复的计算,时间复杂度为O(mn),每删除一个数需要m步运算,有 n 个数)
         2.  使用递归公式, 
               f(n,m)={
                                   0                            (n=1)
                                   (f(n-1,m)+m)%n     (n>1)
                         }
               1. 定义函数f(n,m),n 为剩下的总数,m为删除第m个元素,f(n,m)表示剩下的数字
               2. 删除k后,序列从 k+1开始到k-1,k+1...n-1,0...k-1,映射的序列0,1,2...n-2
               3. 记x为原来的序列,y 为映射后的数组,y=(x-k-1)%n,则 x=(y+k+1)%n
               4. 所以f(n,m)=(f(n-1,m)+m)%n  在n>1时 
            时间复杂度: 线性 O(n)
求1+2+3...+n,要求不能使用乘除法,for,while,if,else,switch,case 等关键字及条件判断语句(A?B:C)
        1.  使用静态变量,创建一个 n大的对象数组,会调用 n 次构造函数(++n,sum+=n),   n,sum都是静态的
        2.  使用函数指针,使用两个函数,一个起递归作用,一个起终止作用,递归函数中数组大小为2,存放的是函数指针,下标0,1分别调用终止函数,递归函数,使用!!n转换成0,1
不用加减乘除做加法
        1. 对两个进行异或
        2. 对两个数相与(只有两个位都为1,相与才为1,所以会有进位),为异或值根据与的有1的位左移一位
       时间复杂度: O(n)
将字符串转为整数(主要考虑健壮性)
      对每位字符进行转换: ‘0’为48
               number=number*10+*string-‘0’
       注意: 1. 字符串地址为NULL(空指针)  return 0;
                2. 字符串为空串(串里有’\0’),return 0;
                3. 如果为负数(判断第一个字符是否为负号,如果为负号将数字转换后,改为负数)
                4. 在数字遍历中遇到’0’~’9’之外的字符,return -1;
                5. 两者都return 0;怎么区分是哪个返回的,设置全局标志位flag即可
树中两个结点的最低公共祖先
         1. 如果这个树是平衡二叉树,当两个节点与树的节点比较,树中的节点位于两个节点的范围内,则这个节点就是这两个树的公共祖先(因为这两个节点为该节点的左右子树上),
             注意: 如果两个节点一个是另一个的孩子时,当遍历到的节点是其中一个节点时,则就是这种情况,它的先序的前一个节点就是他们的公共祖先
          2.  如果这个树不是二叉平衡树但是一颗二叉树,有指向父节点的指针,相当于双向链表,转换为两个链表求第一个公共节点
          3.  如果这棵就是普通的树,先判断两个节点是否在根节点的子树中,如果是true,则继续判断是否在树的孩子节点的子树中,就这样一层一层找下去,直到返回false,则父节点就是最低公共祖先
                这种方法会造成节点的重复计算,有更快的吗?(还有链表的最后公共节点)
                 找到从根节点到要找的两个节点的两条路径,路径存放在栈里(辅存),统计路径的长度,大的减小的的差值,从较大的栈里先取出差值个树,然后两个两个节点进行比较,如果相同就是最低的公共节点
在一个无序的数组中,找第K大的数
          1. 堆排(大数据量时,比如100亿,用堆排)
          2. 选择冒泡(因为求第K大,比较 k趟就可以,所以时间复杂为O(NK)与NlogN相比,用哪个? 如果k 较小的时候(k<logn),那么选择冒泡的效率也挺高的)
          3. 快排(大数据时){
                  1.  找到关键字,将数组分为两部分,右边的部分的值比左边的部分的值大
                  2.  如果k的值大于右边部分的大小,有两种可能,第一种是 k==length+1,则是基准,第二种是k>length+1,则第k大的数在左部分,求出左部分的第k-(length+1)个数,使用快排思想+递归
                  3.  如果k的值小于右部分的大小,则继续使用递归在右部分找
               }
          4. 计数排序(线性排序,适合大数据量处理)       
十进制转八进制
      整数部分对8求余,按从上到下排列,直到整数部分为0,小数部分乘8,取整数,直到小数为0         
        整数部分:  入栈=N%8,N=N/8
        小数部分:   入队(K取整数)=K*8,(k-整数)*8
      如果整数为 

不需要临时变量交换两个数
       只针对数字,如果指针交换,必须用临时变量
       1.  a=a^b(异或),b=a^b,a=a^b,
       2.  a=a+b,b=a-b,a=a-b.
       3、 a=a*b,b=a/b,a=a/b;
快排的优化,
0 0
原创粉丝点击