数据结构题集
一、时间复杂度和空间复杂度
算法效率分析分为两种:第一种是时间效率,第二种是空间效率。
时间效率被称为时间复杂度,而空间效率被称作空间复杂度。 时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间
时间复杂度的计算
时间复杂度是一个衡量算法时间的相对标准。他不是一个固定的时间例如多少分多少秒,而是一个大概的估计数值。比如同一个算法在不同的机器上运行的时间肯定不同。这时候就需要我们的时间复杂度来衡量算法的时间效率
时间复杂度通常使用大O的渐进表示法(空间复杂度也是)
列举几个实例给搭建看看 O(N),O(N^2),O(1)
如上所示就是大O的渐进表示法。括号内就是算法执行的次数,要记住时间复杂度计算的是执行次数
但是他不是一个准确的数字而是一个估计值,比如(N^2+2N+10)在N很大的时候除了最高次项其他的都可以忽略不计,这类似与数学中的极限思想,只保留对结果影响最大的哪一项。
推导法则如下:
1.若括号内是常数都写成O(1)
2.若是括号内是多项式例如O(N^2+5n+2)则只保留最高次项
3.若是最高次项的系数不为1则统一写成1
下面我们来看几个例子学习一下:
// 请计算一下Func1基本操作执行了多少次?
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N ; ++ i)
{
for (int j = 0; j < N ; ++ j)
{
++count;
}
}
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
这个算法的具体执行次数是(N*N+2N+10)而他的时间复杂度是O(N^2)
// 计算strchr的时间复杂度?
const char * strchr ( const char * str, char character )
{
while(*str != '\0')
{
if(*str == character)
return str;
++str;
}
return NULL;
}
要注意有的算法有最坏情况,最好情况和平均情况。比如上面这个算法是在一个字符串中寻找一个字符,最好情况就是一上来就找到了,时间复杂度是O(1),最坏情况是遍历完了字符串才找到或者没找到时间复杂度为O(N),平均情况为O(N/2);
二、线性表
1.输出倒数第k个结点
从头到尾遍历单链表,并用指针q指向当前结点的前k个结点,当遍历到链表尾部时,q所指的结点即为倒数第k个结点
int findNode(LNode *head, int k)
{
LNode *p = head->next, *q = head;
int i = 1;
while(p != NULL)
{
p = p->next;
i++;
if(i > k)
{
q = q->next;
}
}
if (q == head) //没有k个结点
return 0;
else
{
cout << q->data << endl;
return 1;
}
}
三、栈和队列
1.用仅有尾指针的循环单链表实现队列
//入队
void EnQueue(LNode *&rear, int x)
{
LNode *s = (LNode*)malloc(sizeof(LNode);
s->data = x;
s->next = rear->next;
rear->next = s;
rear = s;
}
//出队
int DeQueue(LNode *&rear, int &x)
{
LNode *s;
if(rear->next == rear)
return 0;
else
{
s = rear->next->next; //s指向开始结点
rear->next->next = s->next; //队头元素出栈
x = s->data;
if(s == rear) //若出队后为空
rear = rear->next;
free(s);
return 1;
}
}
三、串
1.KMP算法
void getnext(Str substr, int next[])
{
int i = 1, j = 0;
next[i] = j;
while(i < substr.length)
{
if(j == 0 || substr.ch[i] == substr.ch[j])
{
i++;
j++;
next[i] = j;
}
else
j = next[j];
}
}
int KMP(Str str, Str substr, int next[])
{
int i = 1, j = 0;
while(i <= str.length && j <= substr.length)
{
if(j == 0 || str.ch[i] == substr.ch[j])
{
i++;
j++;
}
else
j = next[j];
}
if(j > substr.length)
return i - substr.length;
else
return 0;
}
//改进
void getnextval(Str substr, int nextval[])
{
int i = 1, j = 0;
nextval[i] = j;
while(i < substr.length)
{
if(j == 0 || substr.ch[i] == substr.ch[j])
{
i++;
j++;
if(substr.ch[i] != substr.ch[j])
nextval[i] = j;
else
nextval[i] = nextval[j];
}
else
j = nextval[j];
}
}
五、树与二叉树
- 树的先序遍历对应于二叉树的先序遍历,树的后序遍历对应于二叉树的中序遍历
- 具有n个结点的完全二叉树的深度为⌊log2n⌋+1
- 用n个值组成一个赫夫曼树,会有2n-1个结点(n + n - 1)
- n个结点的线索二叉树有n-1个线索
- 度为m的赫夫曼树中,叶子结点数为n,则非叶子结点数为:
叶结点即度为0的结点有n个;假设度为m的结点个数为x,则x+n=mx+1;也就是x=n-1/m-1;
若n-1不能被整除,即所给数据不能直接构造最优m叉树,这时需要加一些不影响建树的数据,可以添0;添加的个数为(m-1)-((n-1)%(m-1))。所以最终x应该为⌈n-1/m-1⌉ ,即向上取整;
打印根节点到叶子结点的路径
char pathStack[maxSize]; //路径栈
void printPath(BTNode *p)
{
int i, top = -1; //top为栈顶指针
if(p == NULL) //空树
return;
pathStack[++top] = p->fata; //上图(1)处,p是从上往下走,结点入栈
if(p->lchild == NULL && p->rchild == NULL) //叶子结点
{
//打印路径
for(i = 0; i < top; i++)
cout << pathStack[i] << "->";
}
printPath(p->lchild);
printPath(p->rchild);
top--; //上图(3)处,p是从下往上走,结点出栈
考研真题
1.2020求三元组的最小距离
定义三元组(a,b,c)(a,b,c均为整数)的距离D =∣a−b∣+∣b−c∣+∣c−a∣。
给定3个非空整数集合S1,S2和S3,按升序分别存储在3个数组中。
请设计一种尽可能高效的算法,计算并输出所有可能的三元组(a,b,c)(a∈S1,b∈S2,c∈S3)中的最小距离
例如,S1={-1, 0, 9},S2={-25, -10, 10, 11},S3={2, 9, 17, 30, 41},
则最小距离为2,相应的三元组为(9, 10, 9)。
int isMin(int x, int y, int z){
if(x <= y && x <= z)
return 1;
else
return 0;
}
int main(){
int S1[] = {-1,0,9};
int S2[] = {-25, -10, 10, 11};
int S3[] = {2, 9, 17, 30, 41};
int res[3] = {0, 0, 0};
int Length1 = sizeof(S1)/sizeof(S1[0]);
int Length2 = sizeof(S2)/sizeof(S2[0]);
int Length3 = sizeof(S3)/sizeof(S3[0]);
int D = INT_MAX;
int i = 0, j = 0, k = 0;
while(i < Length1 && j < Length2 && k < Length3 && D > 0){
int dis = abs(S1[i] - S2[j]) + abs(S2[j]- S3[k]) + abs(S3[k] - S1[i]);
if(dis < D){
D = dis;
res[0] = S1[i];
res[1] = S2[j];
res[2] = S3[k];
}
if(isMin(S1[i],S2[j],S3[k])) i++;
else if(isMin(S2[j],S3[k],S1[i])) j++;
else k++;
}
cout << res[0] << " " << res[1] << " " << res[2] << " " << endl;
return 0;
}
2.2019线性表的重新排列
1.用两个指针交替进行,找到链表的中间结点
2.将单链表的后半段逆置
3.从单链表的前后各取一个结点,按要求依次重排
void change_list(NODE* head)
{
NODE * mid,*front, *tail, *temp;
mid = temp = head;
//寻找中间结点
while (temp->next != NULL)
{
mid = mid->next; //mid走一步
temp = temp->next; //temp走两步
if (temp->next != NULL)
temp = temp->next;
}
tail = mid->next; //tail为后半不分链表的首结点
mid->next = NULL;
//后半段逆置
while (tail != NULL)
{
temp = tail->next;
tail->next = mid->next;
mid->next = tail;
tail = temp;
}
front = head->next; //前半段的第一个数据结点,即插入点
tail = mid->next; //后半段的第一个数据结点
//开始重组链表
mid->next = NULL;
while (tail != NULL)
{
temp = tail->next;
tail->next = front->next;
front->next = tail;
front = tail->next;
tail = temp;
}
}
3.2018统计未出现的最小正整数
int findMissMin(int A[], int n)
{
int i, * B; //标记数组
B = (int*)malloc(sizeof(int) * n);//分配空间
memset(B, 0, sizeof(int) * n); //赋初值为0
for (i = 0; i < n; i++)
if (A[i] > 0 && A[i] <= n) //若A[i]的值介于1~n,则标记数组B
B[A[i] - 1] = 1;
for (i = 0; i < n; i++) //扫描数组B,找到目标值
if (B[i] == 0) break;
return i + 1; //返回结果
}
4.2017表达式树转为中缀表达式
void BtreeToE(BTree *root){
BtreeToExp(root, 1); //根的高度为 1
}
void BtreeToExp( BTree *root, int deep)
{
if(root == NULL) return; //空结点返回
else if(root->left==NULL&&root->right==NULL) //若为叶结点
printf(“%s”, root->data); //输出操作数,不加括号
else{
if(deep>l)
printf(“(”); //若有子表达式则加1层括号
BtreeToExp(root->left, deep+1);
printf(“%s”, root->data); //输出操作符
BtreeToExp(root->right, deep+1);
if(deep>l)
printf(“)”); //若有子表达式则加1层括号
}
}
5.2015删除链表的绝对值相等的元素
typedef struct node {
int data;
struct node* link;
}NODE;
typedef NODE* PNODE;
void func(PNODE h, int n)
{
PNODE p = h, r;
int* q, m;
q = (int*)malloc(sizeof(int) * (n + 1)); //申请n+1个位置的辅助空间
for (int i = 0; i < n + 1; i++) //数组元素初值置0
*(q + i) = 0;
while (p->link != NULL)
{
m = p->link->data > 0 ? p->link->data : -p->link->data;
if (*(q + m) == 0) //判断该结点的data是否已出现过
{
*(q + m) = 1; //首次出现
p = p->link; //保留
}
else //重复出现
{
r = p->link; //删除
p->link = r->link;
free(r);
}
}
free(q);
}
6.2014求带权路径长度(WPL)
typedef struct BiTNode {
int weight;
struct BiTNode* lchild, * rchild;
}BiTNode, * BiTree;
int WPL(BiTree root) {
return wpl_PreOrder(root, 0);
}
int wpl_PreOrder(BiTree root, int deep) {
static int wpl = 0; //定义一个 static 变量存储 wpl
if (root->lchild == NULL && root->rchild == NULL) //若为叶子结点,累积 wpl
wpl += deep * root->weight;
if (root->lchild != NULL) //若左子树不空,对左子树递归遍历
wpl_PreOrder(root->lchild, deep + 1);
if (root->rchild != NULL) //若右子树不空,对右子树递归遍历
wpl_PreOrder(root->rchild, deep + 1);
return wpl;
}
7.2013找主元素
排序后的中位数
int majority(int A[], int n)
{
//排序
sort(A,A + n);
int num = 0, temp;
if(n % 2 == 0)
temp = A[n / 2];
else
temp = A[n / 2 + 1];
for(int i = 0; i < n; i++)
{
if(A[i] == temp)
num++;
}
if(num > n / 2)
return temp;
else
return -1;
return 0;
}
8.2011两个升序序列的中位数
算法的基本设计思想如下:
分别求出序列 A 和 B 的中位数,设为 a 和 b,求序列 A 和 B 的中位数过程如下:
① 若 a=b,则 a 或 b 即为所求中位数,算法结束。
② 若 a<b,则舍弃序列 A 中较小的一半,同时舍弃序列 B 中较大的一半,要求舍弃的长度
相等。
③ 若 a>b,则舍弃序列 A 中较大的一半,同时舍弃序列 B 中较小的一半,要求舍弃的长度
相等。
在保留的两个升序序列中,重复过程①、②、③,直到两个序列中只含一个元素时为止,较小者即为所求的中位数。
int M_Search(int A[], int B[], int n) {
int s1 = 0, d1 = n - 1, m1, s2 = 1, d2 = n - 1, m2;
//分别表示序列 A 和 B 的首位数、末位数和中位数
while (s1 != d1 || s2 != d2) {
m1 = (s1 + d1) / 2;
m2 = (s2 + d2) / 2;
if (A[m1] == B[m2])
return A[m1]; //满足条件 1)
if (A[m1] < B[m2]) { //满足条件 2)
if ((s1 + d1) % 2 == 0) { //若元素个数为奇数
s1 = m1; //舍弃 A 中间点以前的部分,且保留中间点
d2 = m2; //舍弃 B 中间点以后的部分,且保留中间点
}
else { //元素个数为偶数
s1 = m1 + 1; //舍弃 A 中间点及中间点以前部分
d2 = m2; //舍弃 B 中间点以后部分且保留中间点
}
}
else { //满足条件 3)
if ((s1 + d1) % 2 == 0) { //若元素个数为奇数
d1 = m1; //舍弃 A 中间点以后的部分,且保留中间点
s2 = m2; //舍弃 B 中间点以前的部分,且保留中间点
}
else { //元素个数为偶数
d1 = m1 + 1; //舍弃 A 中间点以后部分,且保留中间点
s2 = m2; //舍弃 B 中间点及中间点以前部分
}
}
}
return A[s1] < B[s2] ? A[s1] : B[s2];
}
算法的时间复杂度为 O(log2n),空间复杂度为 O(1)
9.2010数组左移
将R中前P个元素逆置,再将其余的元素逆置,最后整体逆置
void reverse(SqList &L)
{
int i,j;
int temp;
for(i = 0, j = L.length; i < j ; ++i, --j)
{
temp = L.data[i];
L.data[i] = L.data[j];
L.data[j] = temp;
}
}
void RCR(int R[], int n, int P)
{
if(p <= 0 || p >= n)
return;
else
{
Reverse(R, 0, P - 1);
Reverse(R, P, n - 1);
Reverse(R, 0, n - 1);
}
}
习题
1.顺序表删除元素
已知长度为n的线性表L采用顺序存储结构,编写一个时间复杂度为O(n),空间复杂度为O(1)的算法,该算法删除线性表中所有值为x的数据元素
void Delnode(sqList &L, ElemType x)
{ int k=0, i;
for(int i=0; i<L.Length(); i++)
if(L.data[i]!=x)
{
L.data[k]=L.data[i];
k++;
}
}
2.链表的就地逆置
就地:辅助空间为O(1)
void Reversel(LinkList*& L)
{
LinkLst* p = L->link, *q;
L->link = NULL;
while(p != NULL)
{
q = p->link; //保存当前节点的下一个节点
p->link = L->link; //更新当前节点的指针域
L->link = p; //更新当前节点上一个节点的位置
p = q; //更新当前节点的位置
}
}
3.表达式匹配
设置一个栈,扫描表达式exp,遇到’(’,’[’,’{’时,将其入栈;遇到’)’,’]’,’}’时,将栈顶符号弹出,与之进行匹配。如果匹配,算法继续进行;否则 表达式的括号不匹配。当算法终止时,栈不为空,exp的括号不匹配;否则,exp成功匹配。
bool Match(char exp[], int n)
{
char st[maxSize];
int i, top = -1; //栈顶指针
bool tag = true;
while(i < n && tag)
{
if(exp[i] == '(' || exp[i] == '[' || exp[i] == '{')
st[++top] = exp[i];
else if(exp == ')')
if(st[top] == '(')
top--;
else
return false;
else if(exp == ']')
if(st[top] == '[')
top--;
else
return false;
else if(exp == '}')
if(st[top] == '{')
top--;
else
return false;
}
if(top >= 0)
return false;
else
return true;
}
4.只有尾指针的循环队列
//入队
void EnQu(Qnode*& rear, ElemType x)
{
Qnode* s = new Qnode;
s->data = x;
if(rear == NULL)
{
s->next = s;
rear = s;
}
else
{
rear->next = s;
rear = s;
}
}
//出队
int DeQu(Qnode*& rear, ElemType& x)
{
Qnode* q;
if(rear == NULL)
return 0;
else if (rear->next == rear)
{
x = rear->data;
delete rear;
rear = NULL;
}
else
{
q = rear->next;
x = q->data;
rear->next = q->next;
delete q;
}
return 1;
}
5.求所有简单路径
从顶点m出发遍历,若找到了顶点v,则输出path;否则找到m的相邻点w,若顶点没有访问,则从w开始递归遍历。
void PathAll1(Graph *G,int u,int m,int v,int path[],int d)
{
ArcNode *p;
int j, w;
if(m == v)
{
for(j = 0; j <= d; j++)
cout << path[j] << ' ';
cout << endl;;
}
else
{
p=G->NodeTable[m].adj;
while(p!=NULL)
{
w=p->dest;
if(visited[w]==0)
{
visited[w]=1;
path[d+1]=w;
Path(G, u, w, v, path, d+1);
visited[w]=0;
}
p=p->link;
}
}
}