链表:空间不连续,顺序查找,定位一个元素的时间为O(N),删除一个元素的时间为O(1)。

数组:连续的存储单元,随机存取(访问可按下标随机访问)。定位元素O(1),删除O(N)。

栈:在表的一端进行插入、删除(先进后出),top=-1(空栈),top=0(栈中只有一个元素),元素进栈top自增,n个元素的入栈问题,可能的出栈顺序为C(2n,n)/(n+1)个。应用:括号匹配,迷宫求解。不是所有的递归转化为非递归都需要用到栈(尾递归、单递归可用到循环)。

队列:在表的一端进行插入,在表的另一端删除,先进先出。队列为空(front==rear),

循环队列:无法通过(front==rear)判断栈满、空。判断队空(front==rear ,引入布尔值)。队满:(rear+1)%size==front。

入队:(rear+1)%size;

串:零个或多个字符组成的有限序列,长度为零的串称为空串,一个或多个空格组成的串称为空白串。

完全二叉树:深度为h的二叉树,(1~h-1)层的节点都达到最大个数,第h层的所有节点都连续集中在最左边。节点i(父节点:i/2,左孩子:2i,右孩子:2i+1)。

二叉树的遍历:

先序遍历

void preOrderRecur(Tree* tree)
{
    if(tree==NULL) return ;
    cout<<tree->data<<" ";
    preOrderRecur(tree->lson);
    preOrderRecur(tree->rson);
}

 申请一个栈,将头结点压入栈 2. 弹出栈顶指针,记作:cur,如果这个这个指针有右孩子,将右孩子入栈,如果有左孩子,将左孩子入栈; 3. 不断重复过程2,直到栈为空,结束程序.

void PreOrderRecur(Tree* tree)
{
    while(!sta.empty()) sta.pop();
    sta.push(tree);
    while(!sta.empty()) {
        Tree* cur  = sta.top();
        sta.pop();
        cout<<cur->data<<" ";
        if(cur->rson!=NULL) sta.push(cur->rson);
        if(cur->lson!=NULL) sta.push(cur->lson);
    }
}

中序遍历、

void inOrderRecur(Tree* tree)
{
    if(tree==NULL) return ;
    inOrderRecur(tree->lson);
    cout<<tree->data<<" ";
    inOrderRecur(tree->rson);
} 

申请一个栈,头结点为开始节点(当前节点) 如果当前节点不为空,那么将左节点压栈,即做tree=tree->lson操作,如果当前节点为空的时候打印栈顶元素,并且出栈,将 当前节点变为栈顶元素的右节点也就是做tree = cur->rson(中序遍历中,栈主要保存的是父节点元素) 不断重复步骤2直到栈空,结束程序! 

void InOrderRecur(Tree* tree)
{
    while(!sta.empty()) sta.pop();
    while(!sta.empty() || tree!=NULL) {
        if(tree==NULL)  {
            Tree* cur = sta.top();
            sta.pop();
            cout<<cur->data<<" ";
            tree=cur->rson;
        } else {
            sta.push(tree);
            tree=tree->lson;
        }
    }
}

后序遍历、

void posOrderRecur(Tree* tree)
{
    if(tree==NULL) return ;
    posOrderRecur(tree->lson);
    posOrderRecur(tree->rson);
    cout<<tree->data<<" ";
}

 申请一个栈,将根节点入栈 如果栈不为空,弹出第一个栈的栈顶元素记做cur,将第一个栈顶元素出栈,然后将cur压入第二个栈。如果cur有左孩子将左孩子加入第一个栈,如果有右孩子将右孩子加入第一个栈 不断的重复步骤2,直到第一个栈为空,打印第二个栈,结束程序!

void PosOrderRecur(Tree* tree)
{
    while(!sta.empty()) sta.pop();
    while(!Sta.empty()) Sta.pop();
    sta.push(tree);
    while(!sta.empty()) {
        Tree* cur = sta.top();
        sta.pop();Sta.push(cur);
        if(cur->lson!=NULL) sta.push(cur->lson);
        if(cur->rson!=NULL) sta.push(cur->rson);
    }
    while(!Sta.empty()) {
        cout<<Sta.top()->data<<" ";
        Sta.pop();
    }
}

层次遍历(队列)。

#include<iostream>
#include<queue>
using namespace std;
void PrintAtlevel(Tree T){
        queue myqueue;
        myqueue.push(T);
        while(!myqueue.empty){
              Tree temp = myqueue.front();
              if(temp->left !=NULL)
                 myqueue.push_back(temp->left);
              if(temp->right !=NULL)
                 myqueue.push_back(temp->right);
              cout << temp->value << " ";
              myqueue.pop();
        }
}

哈夫曼树:带权路径最短的二叉树称为哈夫曼树或最优二叉树;

图的遍历:深度优先搜索(栈)、广度优先搜素(队列)。

排序算法

hash表:适合查找、不适合频繁更新。

常用hash函数:直接定址法、数字分析法、平方取中法、折叠法、除留余数法、随机数法。

冲突解决:开放地址法、开链法。

红黑树:自平衡的二叉查找树。

节点是红色或者黑色,根节点是黑色,每个叶子节点都是黑色的叶子点,每个红色节点的两个子节点都是黑色,从任一节点到每个叶子节点的所有路径都包含相同数目的黑色节点。

调整:变色、旋转。

Dijkstra算法 :使用了广度优先搜索解决赋权有向图或者无向图的单源最短路径问题,算法最终得到一个最短路径树。该算法常用于路由算法或者作为其他图算法的一个子模块。一种贪心的策略。

最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。 

Kruskal算法 :此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。

Prim算法: 此算法可以称为“加点法”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。Prim算法在找当前最近顶点时使用到了贪婪算法。 

设计模式

工厂模式:简单工厂模式、工厂方法模式、抽象工厂模式

简单工厂模式:根据相应的参数,在工厂类中做判断,从而创造相应的产品,当增加产品时,需要修改工厂类。

工厂方法模式:定义一个创建对象的接口,让子类决定实例化哪一个类,使一个类的实例化延迟到其子类。

主要解决:接口选择的问题。

何时使用:明确计划不同条件下创建不用实例时。

如何解决:让子类实现工厂接口,返回的也是一个抽象的产品。

关键代码:创建过程在其子类执行。

缺点:每增加一种产品,就需要增加一个对象工厂,相比简单工厂模式,工厂方法模式需要定义更多的类。

抽象工厂模式:提供一个创建一系列相关或相互依赖的对象接口,而无需指定它们的具体类。

主要解决:主要解决接口选择的问题。

何时使用:系统的产品有多于一个的产品族,而系统只消费其中某一族的产品。

 如何解决:在一个产品族里面,定义多个产品。

关键代码:在一个工厂里聚合多个同类产品。

缺点:产品族扩展非常困难,要增加一个系列的某一产品,既要在抽象的 Creator 里加代码,又要在具体的里面加代码。

策略模式:是指定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。使得算法可以独立于使用它的客户而变化,也就是说这些算法所完成的功能是一样的,对外接口是一样的,只是各自现实上存在差异。

主要解决:在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。

何时使用:一个系统有许多许多类,而区分它们的只是他们直接的行为。

如何解决:将这些算法封装成一个一个的类,任意地替换。

 关键代码:实现同一个接口。

缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。

适配器模式:将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的哪些类可以一起工作。

主要解决:主要解决在软件系统中,常常要将一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的。

何时使用: 1、系统需要使用现有的类,而此类的接口不符合系统的需要。 2、想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。 3、通过接口转换,将一个类插入另一个类系中。(比如老虎和飞禽,现在多了一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容一个虎对象,实现飞的接口。

如何解决:继承或依赖(推荐)。

关键代码:适配器继承或依赖已有的对象,实现想要的目标接口。

缺点:1、过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。

单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决:一个全局使用的类频繁地创建与销毁。

何时使用:想控制实例数目,节省系统资源的时候。

如何解决:判断系统是否已存在单例,如果有则返回,没有则创建。

关键代码:构造函数是私有的。

单例大约有两种实现方法:懒汉与饿汉。

懒汉:故名思义,不到万不得已就不会去实例化类,也就是说在第一次用到类实例的时候才会去实例化,所以上边的经典方法被归为懒汉实现;

构造函数声明为private或者protect防止被外部函数实例化,内部保存一个private static的类指针保存唯一的实例,实例的动作有一个public的类方法实现。 

class singleton   //实现单例模式的类
{
private:
	singleton(){}  //私有的构造函数
	static singleton* Instance;
public:
	static singleton* GetInstance()
	{
		if (Instance == NULL) //判断是否第一调用
			Instance = new singleton();
		return Instance;
	}
};

缺点:这个实现在单线程下是正确的,但在多线程情况下,如果两个线程同时首次调用GetInstance方法且同时检测到Instance是NULL,则两个线程会同时构造一个实例给Instance,这样就会发生错误。

改进的懒汉式(静态内部变量) 在GetInstance函数里定义一个静态的实例,可以保证拥有唯一的实例,在返回是需要返回其指针即可。代码如下: 

class singleton   //实现单例模式的类
{
private:
	singleton() {}  //私有的构造函数
	
public:
	static singleton* GetInstance()
	{
		static singleton Instance;
		return &Instance;
	}
};

改进的懒汉式(双重检查锁),思路:只有在第一次创建的时候进行加锁,当Instance不为空的时候就不需要进行加锁的操作。代码如下:

class singleton   //实现单例模式的类
{
private:
	singleton(){}  //私有的构造函数
	static singleton* Instance;
	
public:
	static singleton* GetInstance()
	{
		if (Instance == NULL) //判断是否第一调用
		{ 
			Lock(); //表示上锁的函数
			if (Instance == NULL)
			{
				Instance = new singleton();
			}
			UnLock() //解锁函数
		}			
		return Instance;
	}
};

饿汉:饿了肯定要饥不择食。所以在单例类定义的时候就进行实例化。

class EagerSingleton   //实现单例模式的类
{
private:
	EagerSingleton(){}  //私有的构造函数
 
	static EagerSingleton Instance = new EagerSingleton();
public:
	static singleton GetInstance()
	{
		return Instance;
	}
};

特点与选择:由于要进行线程同步,所以在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现,可以实现更好的性能。这是以空间换时间。 在访问量较小时,采用懒汉实现。这是以时间换空间。

原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

主要解决:在运行期建立和删除对象。

何时使用:1).当我们的对象类型不是开始就能确定的,而这个类型是在运行期确定的话,那么我们通过这个类型的对象克隆出一个新的对象比较容易一些;2).有的时候,我们需要一个对象在某个状态下的副本,此时,我们使用原型模式是最好的选择;例如:一个对象,经过一段处理之后,其内部的状态发生了变化;这个时候,我们需要一个这个状态的副本,如果直接new一个新的对象的话,但是它的状态是不对的,此时,可以使用原型模式,将原来的对象拷贝一个出来,这个对象就和之前的对象是完全一致的了;3).当我们处理一些比较简单的对象时,并且对象之间的区别很小,可能就几个属性不同而已,那么就可以使用原型模式来完成,省去了创建对象时的麻烦了;4).有的时候,创建对象时,构造函数的参数很多,而自己又不完全的知道每个参数的意义,就可以使用原型模式来创建一个新的对象,不必去理会创建的过程。

->适当的时候考虑一下原型模式,能减少对应的工作量,减少程序的复杂度,提高效率

如何解决:利用已有的一个原型对象,快速地生成和原型对象一样的实例。

关键代码:拷贝,return new className(*this);

模板模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

主要解决:多个子类有相同的方法,并且逻辑相同,细节有差异。

如何解决:对重要,复杂的算法,将核心算法设计为模板方法,周边细节由子类实现,重构时,经常使用的方法,将相同的代码抽象到父类,通过钩子函数约束行为。

关键代码:在抽象类实现通用接口,细节变化在子类实现。

缺点:每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。

判断二叉树是否平衡

二叉树的深度:根节点到叶节点的最长路径长度,平衡二叉树:二叉树中任一节点的左右子树的深度相差不超过1。

平衡因子(1,0,-1):hl-hr.

LL型:一次顺时针旋转;RR型:一次逆时针旋转;LR型:先逆时针,再顺时针;RL型:先顺时针,再逆时针。

递归方法:重复计算,性能不是很好。

public boolean isBalanced(TreeNode root) {
        if(root == null){
            return true;        
        }

        int left = getHeight(root.left);
        int right = getHeight(root.right);
        if(Math.abs(left - right) <= 1 && isBalanced(root.left) && isBalanced(root.right))
            return true;

        return false;
    } 

    public int getHeight(TreeNode node){
        if(node == null)
            return 0;
        int left = getHeight(node.left);
        int right = getHeight(node.right);

        return left > right ? left+1 : right+1;
    }

后序遍历的方法,在遍历每个节点的时候我们已经遍历了它的左右子树,且记录下其深度

class Solution {
public:
    bool IsBalanced(TreeNode *root, int & dep){
        if(root == NULL){
            return true;
        }
        int left = 0;
        int right = 0;
        if(IsBalanced(root->left,left) && IsBalanced(root->right, right))
        {
            int dif = left - right;
            if(dif<-1 || dif >1)
                return false;
            dep = (left > right ? left : right) + 1;
            return true;
        }
        return false;
    }
    bool IsBalanced_Solution(TreeNode* pRoot) {
        int dep = 0;
        return IsBalanced(pRoot, dep);
    }
};

剪枝

public class Solution {
    public boolean IsBalanced_Solution(TreeNode root) {
        return getDepth(root) != -1;
    }
     
    private int getDepth(TreeNode root) {
        if (root == null) return 0;
        int left = getDepth(root.left);
        if (left == -1) return -1;
        int right = getDepth(root.right);
        if (right == -1) return -1;
        return Math.abs(left - right) > 1 ? -1 : 1 + Math.max(left, right);
    }
}

二叉搜索树

二叉搜索树:一棵二叉树,可以为空;如果不为空:非空左子树的所有键值小于其根结点的键值。非空右子树的所有键值大于其根结点的键值。左、右子树都是二叉搜索树。

删除的节点是叶子节点:直接删除 

2.删除的节点只有一个孩子:删除该节点,把该节点的唯一的子节点挂到父节点上

3.该节点是有两个孩子的父节点:我们可以把两个孩子的节点看做是有一个孩子的父节点,但是必须从其子节点找到元素来替换他,下面是找元素替换的方法。

3.1查找该节点左子树的最大元素,把最大元素的值给该节点,然后把那个左子树最大元素的节点删除

3.2查找该节点右子树的最小元素,把最小元素的值给该节点,然后把那个右子树最小元素的节点删除

为什么需要找最大或者最小元素呢,因为这样可以最大或者最小元素可以保证该节点只有一个子节点或者没有节点否则找到一个具有两个节点的父节点,问题还是没有解决。

排序算法

插入排序:将待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

void insert_sort(int array[],unsignedint n)
{
    int temp;
    for(int i = 1;i < n;i++)
    {
        temp = array[i];
        for(int j = i;j > 0;j--)
        {
            if(array[j-1]<=temp)
              break;
            array[j]= array[j - 1];
           
        }
        array[j] = temp;
    }
}

选择排序:

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。重复第二步,直到所有元素均排序完毕.

void select_sort(int *a,int n){
    for(int i = 0;i < n-1;i++)
    {
        int min = i;//查找最小值
        for(int j = i + 1;j < n;j++)
            if(a[min] > a[j])
                min = j;//交换
        if(min != i)
        {
            int t = a[min];
            a[min] = a[i];
            a[i] = t;
        }
    }
}

冒泡排序:比较相邻的元素。如果第一个比第二个大,就交换他们两个。对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。针对所有的元素重复以上的步骤,除了最后一个。持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

void bubble_sort(int a[], int n)
{
    for (int j = 0;j < n - 1;j++)
        for (int i = 0;i < n - 1 - j;i++)
        {
            if(a[i] > a[i + 1])
            {
                int temp = a[i];
                a[i] = a[i + 1];
                a[i + 1] = temp;
            }
        }
}

快速排序:从数列中挑出一个元素,称为 “基准”(pivot),重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

void Qsort(int a[], int low, int high)
{
    if(low >= high)
    {
        return;
    }
    int first = low;
    int last = high;
    int key = a[first];/*用字表的第一个记录作为枢轴*/
 
    while(first < last)
    {
        while(first < last && a[last] >= key)
        {
            --last;
        }
 
        a[first] = a[last];/*将比第一个小的移到低端*/
 
        while(first < last && a[first] <= key)
        {
            ++first;
        }
         
        a[last] = a[first];    
/*将比第一个大的移到高端*/
    }
    a[first] = key;/*枢轴记录到位*/
    Qsort(a, low, first-1);
    Qsort(a, first+1, high);
}

面试之数据结构

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

相关文章:

  • 2021-11-10
  • 2022-01-18
  • 2022-12-23
  • 2021-12-31
  • 2021-10-10
  • 2021-11-06
猜你喜欢
  • 2021-09-18
  • 2021-11-20
  • 2021-07-02
  • 2021-11-14
  • 2021-11-06
  • 2021-08-30
  • 2021-11-06
相关资源
相似解决方案