数据结构相关--面试和笔试高频

数据结构

面试

数组和链表的区别,有什么可以结合二者的优势

数组:存放在连续内存空间上的相同类型数据的集合

链表:通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)

存储空间 存储位置 长度可变性 查询 插入和删除
数组 连续,从栈中静态分配内存 逻辑上相邻的元素在物理存储位置上也相邻 初始给定,会出现溢出现象 下标:O(1) 值:无序—O(n),有序—O(logn) 慢(通过覆盖操作)O(n)
链表 连续或不连续,从堆中动态分配内存(占用相对大,因为包括数据域和指针域) 不一定 动态分配 下标:O(n) 值:O(n) 快O(1)

结合数组查询优势和链表插入和删除优势:哈希表

能够快速判断一个元素是否出现在集合中,通过关键码值**(key value)快速查找,erase()函数删除,insert()函数**插入

红黑树和哈希表的区别,从时间复杂度的角度讲

哈希表,适用于那种查找性能要求高,数据元素之间无逻辑关系要求的情况。每次增删改查的代价可以说是O(1),虽然每次扩容的代价是O(logn)

缺点是,当更多的数插入时,冲突的可能性更大;存储是无序的;resize哈希表的国古城消耗时间(如果现在你的哈希表的长度是100,但是现在有第101个数要插入。这时,不仅哈希表的长度可能要扩展到150,且扩展之后所有的数都需要重新rehash)

红黑树:主要是用它来存储有序的数据,它增删改查的时间复杂度都是O(logn)。是平衡二叉树(AVL)的一种,所以他一定满足根节点小于左子树大于右子树

深度优先和广度优先的区别

1、区别

1) 二叉树的深度优先遍历的非递归的通用做法是采用栈,广度优先遍历的非递归的通用做法是采用队列。

2) 深度优先遍历:对每一个可能的分支路径深入到不能再深入为止,而且每个结点只能访问一次。要特别注意的是,二叉树的深度优先遍历比较特殊,可以细分为先序遍历、中序遍历、后序遍历。具体说明如下:

  • 先序遍历:对任一子树,先访问根,然后遍历其左子树,最后遍历其右子树。
  • 中序遍历:对任一子树,先遍历其左子树,然后访问根,最后遍历其右子树。
  • 后序遍历:对任一子树,先遍历其左子树,然后遍历其右子树,最后访问根。

广度优先遍历:又叫层次遍历,从上往下对每一层依次访问,在每一层中,从左往右(也可以从右往左)访问结点,访问完一层就进入下一层,直到没有结点可以访问为止。

3)深度优先搜素算法:不全部保留结点,占用空间少;有回溯操作(即有入栈、出栈操作),运行速度慢。

广度优先搜索算法:保留全部结点,占用空间大; 无回溯操作(即无入栈、出栈操作),运行速度快。

如何防止数组越界

  • 1)检查传入参数的合法性。
  • 2)可以用传递数组元素个数的方法,即:用两个实参,一个是数组名,一个是数组的长度。在处理的时候,可以判断数组的大小,保证自己不要访问超过数组大小的元素。
  • 3)当处理数组越界时,打印出遍历数组的索引十分有帮助,这样我们就能够跟踪代码找到为什么索引达到了一个非法的值
  • 4)Java中可以加入try{ } catch(){ }

vector的特点

  • 容量在需要时可以自动分配,可以在运行时高效地添加元素,本质上是数组形式的存储方式。
  • 索引查找可以在常数时间内完成。
  • 插入或者删除一项时,需要线性时间。但是在尾部插入或者删除,是常数时间的。

单链表定义构造函数

Struct ListNode{
Int val;//数据域存储的元素
ListNode*next;//指针域指向下一个节点的指针
ListNode(int x):val(x),next(NULL){}//节点的构造函数 空指针必须是大写
}//初始化节点
ListNode*head=new  ListNode(5);

//不用自己定义的构造函数,用c++默认的构造函数来初始化节点,在初始化的时候就不能直接给变量赋值
ListNode *head=new  ListNode();
Head->val=5;

☆手写冒泡排序及快速排序

自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒

// 冒泡排序
	for (int i = 0; i < n-1; ++i) 
	{
		for (int j = 0; j < n - 1 - i; ++j) 
		{
			if (arr[j] > arr[j + 1])  // 从小排到大
			{
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
void quickSort(int a[], int low ,int high)
{
	if(low<high)  //判断是否满足排序条件,递归的终止条件
	{
		int i = low, j = high;   //把待排序数组元素的第一个和最后一个下标分别赋值给i,j,使用i,j进行排序;
		int x = a[low];    //将待排序数组的第一个元素作为哨兵,将数组划分为大于哨兵以及小于哨兵的两部分                                   
		while(i<j)  
		{
		  while(i<j && a[j] >= x) j--;  //从最右侧元素开始,如果比哨兵大,那么它的位置就正确,然后判断前一个元素,直到不满足条件
		  if(i<j) a[i++] = a[j];   //把不满足位次条件的那个元素值赋值给第一个元素,(也即是哨兵元素,此时哨兵已经保存在x中,不会丢失)并把i的加1
		  while(i<j && a[i] <= x) i++; //换成左侧下标为i的元素开始与哨兵比较大小,比其小,那么它所处的位置就正确,然后判断后一个,直到不满足条件
		  if(i<j) a[j--] = a[i];  //把不满足位次条件的那个元素值赋值给下标为j的元素,(下标为j的元素已经保存到前面,不会丢失)并把j的加1
		} 
	        a[i] = x;   //完成一次排序,把哨兵赋值到下标为i的位置,即前面的都比它小,后面的都比它大
		quickSort(a, low ,i-1);  //递归进行哨兵前后两部分元素排序 , low,high的值不发生变化,i处于中间
		quickSort(a, i+1 ,high);
	}
}

概念

将字符串赋值给字符数组

  • 定义时直接用字符串赋值。char a[10]= “hello”;但是不能先定义再赋值,即非法: char a[10];a[10] = “hello”;(因为a是地址常量,已指向堆中分配的10个字符空间,不能再指向数据区的“hello”常量)
  • 利用strcpy,char[10];strcpy(a,’hello’)
  • 利用指针,char*p;p=”hello”(p是地址变量)
  • 数组中的字符逐个赋值。

Java中,String类型的数据存放有两种情况;

如果是String s=“nowcoder”,则是放在字符串常量池中。
如果是String ss=new String(“nowcoder”),则是放在堆中。

char[] f = {'N','O','W','C','O','D','E','R'};
String st = new String(f,2,5);//a = String(b,i,j) a从b中截取 i是开始位置,j是长度 WCODE
char str1[10]={'n','o','w'},str2[]="coder",str3[]="abcde";
strcat(str1,str3);//拼接str1:nowabcde
strcpy(str1+strlen(str2),str2);//把str2的内容复制到str1的第六(5+1)个位置
printf("%d\n",strlen(str1));//nowabcoder

数组中的一些方法

改变原数组

  • push()方法接受任意数量的参数,把它们逐个添加到数组末尾,并返回修改后的数组长度
  • pop()方法从数组末尾移除最后一项,然后返回移除的项
  • shift()方法:移除数组中的第一个项并返回第一个元素的值
  • unshift()方法:在数组前端添加任意个项并返回新数组的长度
    push和shift方法结合使用,可以像队列一样使用数组。
    unshift和pop结合使用,可以从相反的方向来模拟队列:在数组的前端添加项,从数组末端移除项。
  • splice()从数组中添加/删除项目,返回删除的项目
  • join():使用不同的分隔符,返回值是String类型

不改变原数组

slice() 浅拷贝

concat() 复制一样的 但是地址变了

join()

tostring()

树的概念

满二叉树:一棵深度为h,且有2^h-1个节点的树是满二叉树。或除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。

数据结构相关--面试和笔试高频_第1张图片

完全二叉树: 若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h 层所有的结点都连续集中在最左边.

满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。

数据结构相关--面试和笔试高频_第2张图片

最优二叉树(哈夫曼树):在权为wl,w2,…,wn的n个叶子所构成的所有二叉树中,带权路径长度最小(即代价最小)的二叉树。权值较大的节点离根比较近

平衡二叉树:树的左右子树的高度差不超过1的数,空树也是平衡二叉树的一种。

排序算法

数据结构相关--面试和笔试高频_第3张图片

直接插入排序:(适合基本有序)稳定的。时间复杂度:O(n^2)。先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。要点:设立哨兵,作为临时存储和判断数组边界之用

希尔排序:不稳定。先将要排序的一组记录按某个增量d(n/2,n为要排序数的个数)分成若干组子序列,每组中记录的下标相差d.对每组中全部元素进行直接插入排序(比较前后两个的大小,是否交换),然后再用d/2对它进行分组,在每组中再进行直接插入排序。继续不断缩小增量直至为1,最后使用直接插入排序完成排序。

简单选择排序:在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。(若相同的数,要注意第一个和第二个的顺序

堆排序:时间复杂度也为:O(nlogn )。堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置

冒泡排序:在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。

快速排序:(适合无序)不稳定。选择一个基准元素(最佳的基准元素是中位数),通常选择第一个元素或者最后一个元素。通过一趟排序将待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的 元素值比基准值大。

N较大时,当待排序的关键字是随机分布时,快速排序的平均时间最短

当n较小,可采用直接插入或直接选择排序。

直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。

直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序

稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序

不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序

数据结构相关--面试和笔试高频_第4张图片

链表

线性结构:一对一的结构。常用的线性结构有:线性表,栈,队列,双队列,数组,串。

非线性结构,其逻辑特征是一个结点元素可能有多个直接前趋和多个直接后继。常见的非线性结构有:二维数组,多维数组,广义表,树(二叉树等)。

计算题

折半查找的平均查找长度

长度为10,建立判定树

中间记录为(1+10)/2 =5 (注意要取整)

左半区中间值:(1+4)/2 =2 (注意要取整)

右半区中间值(6+10)/2 =8 (注意要取整)

数据结构相关--面试和笔试高频_第5张图片

有序表的长度为n,则外结点一定有n+1个

查找成功:查找每个结点的比较次数之和除以有序表的长度。(11+22+34+43)/10=29/10;

查找失败:查找每个**外结点的比较次数(与正常节点的比较次数)**之和除以外结点的个数。(35+46)/11=39/11

二分(折半)查找

在顺序表 { 12、15、17、20、24、30、38、43、45、51、52} 中,用二分法查找关键码 20需做( ) 次关键字比较。

按下标则是0…10 所以比较3次

1.mid=(0 + 10) / 2 = 5 30

  1. mid=(0 + 4) / 2 = 2 17

3.mid=(3 + 4) / 2 = 3 20

哈希表(散列表)求平均查找长度

已知关键字序列为:(75,33,52,41,12,88,66,27),哈希表长为10,哈希函数为:H(k)=kmod7,解决冲突用线性探测再散列法,要求构造哈希表,求出等概率下查找成功的平均查找长度。

链地址法

首先计算该序列的hash值,H(k)=(5,5,3,6,5,4,3,6)

在这里插入图片描述
数据结构相关--面试和笔试高频_第6张图片

查找成功的平均长度:(4x1+3x2+1x3)/8=13/8

查找不成功的平均长度:

第一次查找不成功:10-4=6

第二次查找不成功:10-3=7

第三次查找不成功:10-1=9

(6x1+7x2+9x3)/8=47/8

线性探测

数据结构相关--面试和笔试高频_第7张图片

查找成功平均长度:(3x1+2x2+1x4+1x5+1x7)/8=23/8

运算符优先级

运算符优先级
单目(-- ++)优先 / , / 优先 +=

++x表示在执行当前语句时,先自增x;
**x++**表示在执行当前语句时,不自增,等到当前行执行完毕后x自增1。

int x=1,y=2,z=3;则表达式y+=z–/++x

所以执行顺序:

x = x+1=2
y = y+z/x=2+3/2=3
z = z-1=2

循环队列

假设循环队列的队尾指针是rear,队头是front,其中QueueSize为循环队列的最大长度。

(1) 入队时队尾指针前进1:(rear+1)%QueueSize

(2) 出队时队头指针前进1:(front+1)%QueueSize

(3) 队列长度:(rear-front+QueueSize)%QueueSize

(4) 队空和队满的条件

方式1: 约定以"队头指针在队尾指针的下一位置作为队满的标志"。

队满条件为:(rear+1)%QueueSize==front

队空条件为:front==rear

队列长度为:(rear-front++QueueSize)%QueueSize

方式2: 增设表示队列元素个数的数据成员size,此时,队空和队满时都有front==rear

队满条件为:size==QueueSize

队空条件为:size==0

方式3: 增设tag数据成员以区分队满还是队空

tag表示0的情况下,若因删除导致front==rear,则队空;

tag等于1的情况,若因插入导致front==rear则队满

例如:循环队列的存储空间为 Q(1:40) ,初始状态为 front=rear=40 。经过一系列正常的入队与退队操作后, front=rear=15 ,此后又退出一个元素,则循环队列中的元素个数为(39或0,且产生下溢错误)。

当 front=rear=15 时可知队列空或者队列满,如果之前队列为空,退出操作会产生错误,队列里有 0 个元素;如果退出之前队列已满 (40 个元素 ) ,执行退出后,队列里还有 39 个元素。

给定入栈顺序,求所有可能的出栈顺序

卡特兰数 C(n,2n)/(n+1)=(2n)!/(n! * (n+1)!)

数组存储

  • 普通二维矩阵

以行优先顺序存储三维数组A[5][6][7],其中元素A[0][0][0]的地址为1100,且每个元素占2个存储单元,则A[4][3][2]的地址是(1482)

思考一个立方体。A[5][6][7]就是一个长6宽7高5的立方体。A[4][3][2]出于立方体的第5层的第四前两个行的第三个元素。前四层是满的,一共有467=168个元素,第五层前三行是满的再加上第四行的,有37+2=23个元素。因此,在A[4][3][2]之前共有168+23=191个元素。因此,地址是,1100+1912=1482

设有数组A[n,m],数组的每个元素长度为3字节,n的值为1~8,m的值为1~10,数组从内存收地址0开始顺序存放,请分别用列存储方式和行存储方式求A[5,8]的存储首地址为多少

按照行:(4*10+7)*3

按照列:(7*8+4)*3

  • 对称矩阵

设有一个10阶的对称矩阵A,采用压缩存储方式,以行序为主存储,a11为第一元素,其存储地址为1,每个元素占一个地址空间,则a85的地址为(33 )。

1+…+7+5

行:i(i-1)/2+j*

列**:j*(j-1)/2+i**

第i行的对角元素A[i][i]在B中的存放位置 (i+3)×i/2 第0行有1个,第i行有i+1个,总和(1+i+1)(i+1)/2

  • 稀疏矩阵

表示方法:三元组、十字链表、行连接的顺序表

例如:有一个100*90的稀疏矩阵,非0元素有10个,设每个整型数占2字节,则用三元组表示该矩阵时,所需的字节数是(66)

非零元素 1032+还保存的有行数、列数、非零元素数 321=6

求表头表尾

GetHead是取广义表的第一个元素,要去掉一个"()",

GetTail是除掉第一个元素,"()"不去除。

GetHead【((a,b),(c,d))】→(a,b)

GetHead【GetTail【((a,b),(c,d))】】→GetHead【((c,d))】→(c,d)

求长度和深度问

长度:去掉一层括号后还剩几部分

深度:去掉几层括号后可以看到最后一部分

((a,b),(c,d)) 长度为2,深度为2

原码、反码、补码

对于正数:原码=反码=补码
对于负数:反码=原码除符号位其他位取反,补码=反码+1

链表插入删除问题

双向

在p后插入s

s→left=p,s→right=p→right,p→right→left=s,p→right=s

在p前插入s

s→right=p,s→left=p→left,p→left→right=s,p→left=s

删除p

p→next→prior=p→prior,p→prior→next=p→next

单向

r插入p后:r->next=p->next;p->next=r

删除P节点的直接后继节点:P->next=P->next->next;

KMP 字符串求next数组

Kmp算法,时间复杂度O(m+n),空间复杂度O(n),需要的辅助空间O(m),注意,n为主串的长度,m为模式串长度

例如:已知串S=‘aaab’,其Next数组值为( )

求出共有元素长度0120

然后整体右移一位,左边补-1,再整体+1;因此为0123

  • KMP:当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。

前缀:指不包含最后一个字符的所有以第一个字符开头的连续子串。

后缀:指不包含第一个字符的所有以最后一个字符结尾的连续子串。

前缀表:相同的前后缀长度

模式串与前缀表对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。

next数组:记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配;

next[j]:其值=第j位字符前面j-1位字符组成的子串的前后缀重合字符数+1

所以会经过处理:前缀表统一减一(右移一位,初始位置为-1)

前缀与后缀细讲(共有长度就是前缀表的数值)

下面再以”ABCDABD”为例,进行介绍:

- ”A”的前缀和后缀都为空集,共有元素的长度为0;

- ”AB”的前缀为[A],后缀为[B],共有元素的长度为0;

- ”ABC”的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

- ”ABCD”的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

- ”ABCDA”的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为”A”,长度为1;

- ”ABCDAB”的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为”AB”,长度为2;

- ”ABCDABD”的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

next数组的构造

KMP算法之求next数组代码讲解_哔哩哔哩_bilibili

数据结构相关--面试和笔试高频_第8张图片

void getNext (int* next, const string& s){
    next[0] = -1;//初始化0下标的next数组值为-1
    int j = -1;//代表前缀末尾的位置 和 最长相等前后缀的长度  记录该下标对应的next数组的值
    for(int i = 1;i < s.size(); i++){//处理前后缀不相同,连续回退
      while(j >= 0 && s[i] != s[j+1]) {
        j = next[j];
      }
      if(s[i] == s[j+1]) {//处理相同
        j++;
      }
      next[i] = j;//更新
    }
}
https://blog.csdn.net/github_39533414/article/details/89337910
// 在字符串s中匹配字符串t,s长度为len_s,t长度为len_t,t的Next数组为next[],返回t第一次出现的位置
int find_pattern(char s[], int len_s, char t[], int len_t, int next[])
{
    int i = 0, j = 0;
    while(i < len_s && j < len_t)//对两串进行扫描
   {
		if(j == -1 || s[i] == t[j]) //对应字符匹配	
		{
			j++;
			i++;			
		}
		else{
			   j = next[j];//失配时,用next数组值选择下一次匹配的位置
		}
	}
		
		if(j == len_t)
		  return i - j ;//匹配成功 
		else
		   return -1;
    
}
 
int main()
{
    int cas;
    char s[MAX_LEN], t[MAX_LEN];
    int next[MAX_LEN];
    scanf("%d", &cas);
    while (cas --)
    {
        scanf("%s %s", s, t);
        int len_s = strlen(s);
        int len_t = strlen(t);
        get_next(t, len_t, next);
        printf("%d\n", find_pattern(s, len_s, t, len_t, next));
    }
    return 0;

求子串的数目

子串:串中任意个连续的 字符 组成的子序列

子串: n(n+1)/2 + 1
真子串(不包含自身)=非空子串(字符串长度至少为1,不包括空串):n(n+1)/2
非空真子串:n(n+1)/2 - 1

例如:串S=“software”,子串=37,真子串=36,非空真子串=35。

"www.qq.com"所有非空子串(两个子串如果内容相同则只算一个)个数是(50)10*(10+1)/2=55,

…重复了一个;qq也是重复了一个,www重复了3个(w ww www w ww w,n(n-1)/2=3*)

哈夫曼编码

“mabnmnm” 的二进制进行哈夫曼编码有13位

字符串 "mabnmnm"每个字符的频率分别为:m 3/7,a 1/7,b 1/7,n 2/7。

数据结构相关--面试和笔试高频_第9张图片

a:111,b:110,n:10,m:0。
字符串 “mabnmnm” 的哈夫曼编码为 0 111 110 10 0 10 0。
一共13位,所以正确答案选B。

构造哈夫曼树

并不唯一,但是带权路径长度是相同的

将数组按照从小到大排列 每次都选出最小的两个数字

3 5 7 8 11 14 23 29

数据结构相关--面试和笔试高频_第10张图片

带权路径长度 根结点到该结点之间的路径长度与该结点的权的乘积 :(3+5)*5+(7+8+11)*4+(14+23)3+292

求排列组合的方式

C(9,2)=A(9,2)/A(2,2)=98/21=36

A(9,2)=9!/(9-2)!

“qiniu” 根据顺序不同有多少种排列组合的方式

A(5,5)/2=60 因为有两个重合的

你可能感兴趣的:(测试开发,数据结构,深度优先,算法)