面试基础知识

目录

前言

从CC150这本书的前几个章节介绍了一些必须掌握的基础知识,在这里就做个学习的总结。

数据结构

链表

常规的链表操作,还有就是快慢指针的应用和首尾指针。

二叉树

[待更新]

单词查找树

Tire树,又称之为字典树或者单词查找树。是一种树形结构,是哈希树的变种。典型应用是用于统计、排序或保存大量的字符串(不仅限于字符串),所以经常被搜索引擎系统用于文本词频的统计。因为相同的字符串前缀会共享同一条分支,所以优点是可以利用不同字符串的相同前缀来减少无谓的字符串比较,查找效率比hash表/hash树高。

有以下的性质:

1.除了根节点以外,每个节点都包含一个字符;
2.从根节点到当前节点路径上的字符连接起来组成该节点对应的字符串;
3.每个节点的所有子节点所包含的字符是不相同的

图片.png

如果在查找过程中终止于内部结点,说明没有找到待查找字符串。

应用场景可以是这样的,事先将已知的一些字符串(字典)的有关信息保存到trie树中,查找另外一些位置字符是否出现过或者出现频率。

  1. 最长公共前缀问题:给定N个字符串,求两个串的最长公共前缀;两个字符串的最长公共前缀的长度即由根节点到最近公共祖先节点路径上字符的个数。
给出N 个小写英文字母串,以及Q 个询问,即询问某两个串的最长公共前缀的长度是多少.  解决方案:首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线  (Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。 

而最近公共祖先问题同样是一个经典问题,可以用下面几种方法: 
    (1)利用并查集(Disjoint Set),可以采用采用经典的Tarjan 算法; 
    (2)求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;
  1. 给出N个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。思路:将N个单词的熟词表建立一棵Tire树,对于文章中的每个单词,查找其在Tire树中是否存在。

  2. 实现映射,比如同一个东西有两种表示方法A和B,现在给你了A,让你转换成B。最容易想到的是hash,用Trie树实现也是比较简单的,可以在Trie数据结构中新增一项string Data_B;这样如果IsWord=true,访问Data_B即可获取方法B的表示。

  3. 有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。

  4. 给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。

  5. 1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串

Stack,先进后出

队列

Queue,先进先出

向量/数组列表

Vector,动态数组

散列表

Hashtable,插入、查找速度都是不错的。

优点:不论哈希表中有多少数据,查找、插入、删除(有时包括删除)只需要接近常量的时间即0(1)的时间级。实际上,这只需要几条机器指令。哈希表运算得非常快,在计算机程序中,如果需要在一秒种内查找上千条记录通常使用哈希表(例如拼写检查器)哈希表的速度明显比树快,树的操作通常需要O(N)的时间级。哈希表不仅速度快,编程实现也相对容易。

如果不需要有序遍历数据,并且可以提前预测数据量的大小。那么哈希表在速度和易用性方面是无与伦比的。

缺点:它是基于数组的,数组创建后难于扩展,某些哈希表被基本填满时,性能下降得非常严重。

算法

广度优先搜索

深度优先搜索

二分查找

复杂度\(O(log_{2}n)\)

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

二分查找只适用顺序存储结构

归并排序

复杂度\(O(nlog(n))\)

图片.png

归并排序的效率是比较高的,设数列长为N,将数列分开成小数列一共要logN步,每步都是一个合并有序数列的过程,时间复杂度可以记为\(O(N)\),故一共为\(O(N*logN)\)

图片.png

快速排序

快速排序是一种既不浪费空间时间效率也较好的一种排序算法

快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是\(O(n^{2})\),它的平均时间复杂度为\(O(nlogn)\)

图片.png

这一篇博客讲得非常浅显易懂

数的插入/查找

用map或者是哈希表,他们的插入、查找、删除操作都比较快。

用map的原因是,map是基于红黑树实现的,而本身红黑树的查找、插入效率较高。

而哈希表,或者是散列表,是根据关键码值(key value)而进行直接访问的数据结构。它通过关键码值映射到表中的一个位置来访问记录,以加快访问的速度。这个映射函数叫做散列函数,存放的数组叫做散列表。 更加详细的可以参考这一篇博客.

KMP算法

博客

概念

位操作

位操作只能用于整形数据,对float和double类型进行位操作会被编译器报错。位操作能比正常运算的速度要快,因为涉及到了底层。比如常见的交换操作:

void Swap(int &a, int &b)  
{  
    if (a != b)  
    {  
        a ^= b;  
        b ^= a;  
        a ^= b;  
    }  
}  

单例设计模式

参考博客1参考博客2

单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中一类只有一个实例而且该实例易于外界访问,从而达到使用目的(如windows操作系统中,任务管理器只能打开一个--主要目的),同时还能方便对实例个数的控制并节约系统资源(主要目的之外的好处)。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。

图片.png

如何保证一个类只有一个实例并且这个实例易于被访问呢?定义一个全局变量可以确保对象随时都可以被访问,但不能防止我们实例化多个对象。一个更好的解决办法是让类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。这就是单例模式的模式动机。

主要有两点需要特别注意:一点是构造方法必须是private的,否则不能保证只有一个实例;另一个是必须有public static的获取实例方法或者public static的实例属性,保证外部能访问。

总结一下:

单例是为了保证系统中只有一个实例,其关键点有以下:

一.私有构造函数

二.声明静态单例对象

三.构造单例对象之前要加锁(lock一个静态的object对象)

四.需要两次检测单例实例是否已经被构造,分别在锁之前和锁之后

一段代码:

public class Singleton {  
    private Singleton() {}                     //关键点0:构造函数是私有的
    private static Singleton single = null;    //关键点1:声明单例对象是静态的
    private static object obj= new object();
    public static Singleton GetInstance()      //通过静态方法来构造对象
    {                        
         if (single == null)                   //关键点2:判断单例对象是否已经被构造
         {                             
            lock(obj)                          //关键点3:加线程锁
            {
               if(single == null)              //关键点4:二次判断单例是否已经被构造
               {
                  single = new Singleton();  
                }
             }
         }    
        return single;  
    }  
}

工厂设计模式

参考博客

工厂模式,实际上也会根据业务情景不同会有不同的实现方式。一般分为3种。简单工厂,工厂和抽象工厂。

产品和工厂需要完全解耦 。从代码角度其实就是将冗长的代码分解成为一段又一段非常小的程序段。

内存(栈和堆)

栈内存:栈内存首先是一片内存区域,存储的都是局部变量,凡是定义在方法中的都是局部变量(方法外的是全局变量),for循环内部定义的也是局部变量,是先加载函数才能进行局部变量的定义,所以方法先进栈,然后再定义变量,变量有自己的作用域,一旦离开作用域,变量就会被释放。栈内存的更新速度很快,因为局部变量的生命周期都很短。

堆内存:存储的是数组和对象(其实数组就是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象),实体用于封装数据,而且是封装多个(实体的多个属性),如果一个数据消失,这个实体也没有消失,还可以用,所以堆是不会随时释放的,但是栈不一样,栈里存放的都是单个变量,变量被释放了,那就没有了。C++里需要手动进行清除,如用free或者析构函数、delete进行删除,否则会发生内存溢出现象。堆里的实体虽然不会被释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取。这也是C++与Java的区别之一。

再说一下堆和栈是如何联系起来的,我们在创建堆的时候开辟一块空间(连续的二进制),然后给这块空间分配一个内存地址。这时将栈中的变量指向这块内存地址,所以如果想要通过栈访问堆实质上是引用了堆内存当中的实体。

总结一下:

1.栈内存存储的是局部变量而堆内存存储的是实体;

2.栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短;

3.栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。

递归

递归算法,可能考察的点在动态规划问题上。

大O时间

几种复杂度的计算,了解渐进临界值和取最大幂数即可。

猜你喜欢

转载自www.cnblogs.com/yunlambert/p/9276488.html