线性表
线性表是由 \(n\)个元素\((n \ge 0)\)组成的有限序列,线性表的特征
- 所有数据元素类型相同
- 线性表是由有限个元素构成
- 线性表中的数据元素是与位置是有关的,这一点表明线性表不同于集合,线性表中每个元素都有一个对应的序号,线性表中的元素可以重复出现。
线性表的逻辑结构一般表示为:\( a_1,a_2,...,a_{i-1},a_i,a_{i+1},...,a_n \)
- 除起始元素没有\(a_1\) 没有前驱元素之外,其他元素\(a_i\)有且仅有一个前驱元素\(a_{i-1},\)
- 除终端元素没有后继元素外,其他元素 \(a_i\) 有且仅有一个后继元素\(a_{i+1}\)
线性表是一种逻辑结构,线性表的存储结构有
- 顺序存储
- 链式存储
顺序表
顺序表的特点
- 属于直接映射(逻辑上相邻的元素,其物理位置也相邻),
- 具有随机存取特性( 通过首地址和元素序号可以在 \(O(1)\) 时间内找到指定元素 )
- 删除和插入元素需要移动大量元素。
- 插入元素移动的平均次数\(=\frac{1}{n}\sum_{i=1}^{n}(n-i+1)=\frac{n}{2}\), 因此时间复杂度为\(O(n)\)
- 删除元素移动的平均次数\(=\frac{1}{n}\sum_{i=1}^{n}(n-i)=\frac{n-1}{2}\),因此时间复杂度为 \(O(n)\)
- 存储密度高,其值为\(1\)。\(存储密度=节点数据本身所占用的存储量/节点结构占用的存储量\)
描述顺序结构的三个属性
- 顺序表的起始地址
- 顺序表的最大长度
- 顺序表的当前长度
# define MaxSize 100
typedef struct
{
int data[MaxSize];
int length;
} SqList;
给定一个有 \(n\) 个元素的整型数组\(arr\),其中连续的相等元素构成的子序列称为平台,设计一个算法求 \(arr\)中最长平台的长度。
int maxLength(int arr[], int n)
{
int len = 1;
int max = 1;
int start = 0;
for (int i = 1; i < n; i++)
{
if (arr[i] == arr[start])
len++;
else
{
if (len > max)
max = len;
start = i;
len = 1;
}
}
if (len > max)
max = len;
return max;
}
设有一个顺序表\(L\), 其元素为整型数据,设计一个算法将\(L\)中所有小于\(0\)的整数放在前半部分,大于等于\(0\)的整数放在后半部分。
void change(SqList &L)
{
int tmp;
int i = 0, j = L.length - 1;
while (i < j)
{
while (i < j && L.data[i] < 0)
i++;
while (i < j && L.data[j] >= 0)
j--;
if (i < j)
{
tmp = L.data[i];
L.data[i] = L.data[j];
L.data[j] = tmp;
}
}
}
设有一个顺序表\(L\),其元素为整型。设计一个尽可能高效的算法,以第一个元素为分界线,将所有小于等于它的元素移到该元素前面,将所有大于它的元素移到该元素的后面。
void move(SqList &L)
{
int i = 0;
j = L.length - 1;
int pivot = L.data[0];
int tmp;
while (i < j)
{
while (i < j && L.data[j] > pivot)
j--;
while (i < j && L.data[i] <= pivot)
i++;
tmp = L.data[i];
L.data[i] = L.data[j];
L.data[j] = tmp;
}
if (L.data[i] < pivot)
{
tmp = L.data[0];
L.data[0] = L.data[i];
L.data[i] = tmp;
}
}
void move(SqList &L)
{
int i = 0, j = L.length - 1;
int pivot = L.data[0];
while (i < j)
{
while (i < j && L.data[i] > pivot)
j--;
L.data[i] = L.data[j];
i++;
while (i < j && L.data[i] <= pivot)
i++;
L.data[j] = L.data[i];
j--;
}
L.data[i] = pivot;
}
void move(SqList &L)
{
int i = 0;
int pivot = L.data[0];
int tmp;
for (int j = 1; j < L.length; j++)
{
i++;
if (i != j)
{
tmp = L.data[i];
L.data[i] = L.data[j];
L.data[j] = tmp;
}
}
tmp = L.data[0];
L.data[0] = L.data[i];
L.data[i] = tmp;
}
已知长度为 \(n\) 的线性表 \(L\) 采用顺序存储结构,编写一个时间复杂度为\(O(n)\),空间复杂度为\(O(1)\) 的算法,该算法删除线性表中所有值为\(x\)的数据元素。
void deleteSameElements(SqList &L, int x)
{
int base = 0;
for (int i = 0; i < L.length; i++)
{
if (L.data[i] != x)
{
L.data[base] = L.data[i];
base++;
}
}
L.length = base;
}
void deleteSameElements(SqList &L, int x)
{
int count = 0;
int i = 0;
while (i < L.length)
{
if (L.data[i] == x)
count++;
else
L.data[i - count] = L.data[i];
i++;
}
L.length = L.length - count;
}
已知长度为 \(n\) 的线性表 \(L\) 采用顺序存储结构,试设计一个时间复杂度空间复杂度两方面都尽可能高效的算法,该算法删除线性表中元素值为\([x,y]\)之间的所有数据元素。
单链表
单链表是用任意一组存储单元存放线性表中的元素,每个节点通过一个指针指向后继节点。这组存储单元可以是连续的也可以是不连续的。
- 通过头节点(带头节点的单链表)或首节点(不带头节点的单链表)的指针来标识一个链表
- 从一个已知节点出发,只能访问该节点和通过\(next\)指针访问其后继节点,无法直接找到该节点之前的其他节点
- 在单链表中插入一个节点或者删除一个节点必须已知其前驱节点,插入和删除操作不需要移动节点
typedef struct LinkedNode
{
T data; //T表示数据类型
struct LNode* next;
}LinkedList;
建立单链表的两种方式
头插法「逆序了」
void creatLinkedList(LinkedList* &L, int a[],int n) { LinkedList* s; L=(LinkedList*)malloc(sizeof(LinkedList)); L->next=NULL; for(int i=0;i<n;i++) { s=(LinkedList*)malloc(sizeof(LinkedList)); s->data=a[i]; s->next=NULL; L->next=s; } }
尾插法「还是原来的顺序」
void creatLinkedList(LinkedList* &L, int a[],int n) { LinkedList* s,*r; L=(LinkedList*)malloc(sizeof(LinkedList)); r=L; //r始终指向终端节点,开始时指向头结点 for(int i=0;i<n;i++) { s=(LinkedList*)malloc(sizeof(LinkedList)); s->data=a[i]; r->next=s; r=s; } r->next=NULL; }
单链表的基本操作
按序号查找节点值算法
int findNode(LinkedList *L,int i,int &e) { int j=0; LinkedList *ptr=L; while(ptr!=NULL && j<i) { j++; ptr=ptr->next; } if(NULL==ptr) return 0; else { e=ptr->data; return 1; } }
按元素值查找序号算法
int findNode(LinkedList *L,int key) { LinkedList *ptr=L->next; int index=0; while(ptr!=NULL && ptr->data!=key) { index++; ptr=ptr->next; } if(ptr!=NULL) return index; else return 0; }
插入元素:将值为\(x\)的元素的新节点插入到第\(i\)个节点的位置上,即先在单链表中找到插入节点的前驱节点,即第\(i-1\)个节点,再在其后插入新节点。
s->next=p->next; p->next=s;
删除元素:将单链表中的第 \(i\) 个节点删除。
q=p->next; p->next=q->next; free (q);
有一个线性表\((a_1,a_2,...,a_n)\) 采用带头节点的单链表\(L\)存储,设计一个就地算法将其就地逆置,所谓“就地”是指算法的辅助空间为\(O(1)\).
void reverse(LinkedList * & L)
{
LinkedList *ptr=L->next;
LinkedList *q;
L->next=NULL;
while(ptr!=NULL)
{
q=ptr->next;
ptr->next=L->next;
L->next=ptr;
ptr=q;
}
}
设\(C=\{a_1,b_1,a_2,b_2,...,a_n,b_n\}\) 为一线性表,采用带头节点的\(hc\)单链表存放,设计一个就地算法,将其拆分为两个线性表(它们都是用单链表存放)使得\(A=\{a_1,a_2,...,a_n\},B=\{b_1,b_2,...,b_n\}\)
void split(LinkedList *hc,LinkedList *&ha,LinkedList *&hb)
{
LinkedList *r=ha;
LinkedList *p=hc->next;
LinkedList *q;
hb->next=NULL;
while(p!=NULL)
{
r->next=p;
r=p;
p=p->next;
q=p->next;
p->next=hb->next;
hb->next=p;
p=q;
}
r->next=NULL; //别忘了
}
王道题讲解
有一个带头结点的单链表\(L\),设计一个算法使其元素递增有序。
void sort(LinkedList * &L)
{
LinkedList *p=L->next;
LinkedList *q=p->next;
LinkedList *pre,tmp;
p->next=NULL: //含有一个元素的有序单链表
p=q;
while(p!=NULL)
{
q=p->next;
pre=L; //单链表只能从头开始往后寻找节点
tmp=pre->next;
while(tmp!=NULL && tmp.data < p->data)
{
pre=tmp;
tmp=tmp->next;
}
p->next=pre->next;
pre->next=p;
p=q;
}
}
给定两个单链表,编写算法找出其公共的节点
分析:从头到尾扫描单链表\(A\),判断当前元素是否在单链表\(B\)中出现,若在则插入到单链表\(C\)中。
void findSameNode(LinkedList *A,LinkedList *B,LinkedList *&C)
{
LNode *p=A->next;
LNode *q=B->next;
LNode *r;
C=(LNode*)malloc(sizeof(LNode));
C->next=NULL;
r=C;
while(p!=NULL)
{
q=B->next;
while(q!=NULL && q.data!=p.data)
q=q->next;
if(q!=NULL)
{
r->next=p;
r=p;
}
p=p->next;
}
}
\(2010\)年联考真题
设将\(n\) 个整数存放到一维数组R中。试设计一个在时间和空间尽可能高的算法。将\(R\)中保存的序列循环左移\(p (0 \lt q\lt n)\)个位置,即将\(R\)中的数据由\((X_0,X_1,...,X_{n-1})\) 变换为\((X_p,X_p+1,...,X_{n-1},X_0,X_1,....,X_{p-1})\)
void reverse(int R[],int left,int right)
{
int i=left,j=right;
int tmp;
while(i<j)
{
tmp=R[i];
R[i]=R[j];
R[j]=tmp;
i++;
j--;
}
}
void leftShift(int R[],int n,int p)
{
if(p>0 &&p<n)
{
reverse(R,0,n-1);
reverse(R,0,n-p-1);
reverse(R,n-p,n-1);
}
}
\(2011\)年联考真题
给定两个数组\(A\)和\(B\),数组的长度为\(n\),两个数组都分别有序,求出两个数组中的所有数的中位数。
int search(int A[],int B[],int n)
{
int i,j,k;
i=j=k=0;
while(i<n &&j<n)
{
k++;
if(A[i]<B[j])
{
if(k==n)
return A[i];
i++;
}
else
{
if(k==n)
return B[j];
j++;
}
}
}
分别求出两个升序序列\(A\),\(B\)的中位数,记为\(a\),\(b\)。若\(a=b\),则\(a\)或\(b\)即为所求,否则舍弃\(a,b\)中较小者所在序列的较小一半,同时舍弃较大者所在序列的较大一半,要求两次舍弃的元素个数相同(每次从左侧和右侧删除相同个数的元素后,新的两个数组,它们的中位数与原始数组的中位数是相同的)。重复上述过程,直到两个序列均只含一个元素为止,则较小的即为所求的中位数。
int searchMid(int A[],int B[],int n)
{
int startA,midA,endA;
int startA,midB,endB;
startA=0; endA=n-1;
startB=0; endB=n-1;
while(startA!=endA || startB!=endB)
{
midA=(startA+endA)/2;
midB=(startB+endB)/2;
if(A[midA]==B[midB])
return A[midA];
if(A[midA]<B[midB])
{
if((startA+endA)%2==0)//若元素个数为奇数时
{
startA=midA; //舍弃A中间点以前的部分且保留中间点
endB=midB; //舍弃B中间点以后的部分且保留中间点
}
else //若元素个数为偶数时
{
startA=midA+1; //舍弃A的前半部分,每次舍弃的长度相同,可以保证同时到达
endB=midB; //舍弃B的后半部分
}
}
else if(A[midA] > B[midB])
{
if((startA+endA)%2==0)//若元素个数为奇数时
{
endA=midA; //舍弃A中间点以后的部分且保留中间点
startB=midB;//舍弃B中间点以前的部分且保留中间点
}
else //若元素个数为偶数时
{
endA=midA; //舍弃A的后半部分
startB=midB+1; //舍弃B的前半部分
}
}
}
return A[startA]<B[startB]?A[startA]:B[startB];
}
- 若\(A\)和\(B\)数组的长度为\(2k+1\),取\(A\)数组的中位数为\(A[k]\),\(B\)数组的中位数为\(B[k]\),\(A\)和\(B\)组合起来的中位数应该是第\(2k+1\)大的那个数。如果\(A[k]==B[k]\),则\(A[k]\)必定为第\(2k+1\)大的数,是所有数字的中位数。如果\(A[k]\gt B[k]\),则\(A[k]\)至少为第\(2k+2\)大的数,\(B[k]\)至多为第\(2k+1\)大的数,中位数介于\(B[k]\)和\(A[k]\)之间。
- 若\(A\)和\(B\)数组的长度为\(2k\),按照题目所述条件,则\(A\)的中位数为\(A[k-1]\),则\(B\)的中位数为\(B[k-1]\),\(A\)和\(B\)组合起来的中位数应该是第\(2k\)大的那个数,若\(A[k-1]==B[k-1]\),则\(B[k-1]\)必为第\(2k\)大的那个数,即所有数字的中位数。如果\(A[k-1]\gt B[k-1]\),则\(A[k-1]\)至少为第\(2k\)大的数,\(B[k-1]\)至多为第\(2k-1\)大的数,中位数介于\(B[k-1]\)和\(A[k-1]\)之间。
\(2013\)年联考真题

主元素问题方法一:对数组中元素进行计数,然后查看出现次数最多的元素,若次数大于一半,则为主元素。这种方式只需要对数组扫描一遍,时间复杂度为\(O(n)\),空间复杂度为\(O(n)\)。
int mainElement(int A[],int n)
{
int *p=(int*)malloc(sizeof(int)*n);
int max=0;
for(int i=0;i<n;i++) p[i]=0;
for(int i=0;i<n;i++)
{
p[A[i]]++;
if(p[A[i])>p[A[max]]) max=A[i];
}
if(p[max]>(n/2) return max;
else return -1;
}
使用快速排序,然后统计相同元素出现的最大次数
void quickSort(int A[],int left,int right)
{
int i=left,j=right;
int tmp;
if(i<j)
{
tmp=A[i];
while(i<j)
{
while(i<j && A[j]>tmp) j--;
A[i]=A[j];
i++;
while(i<j && A[i]<=tmp) i++;
A[j]=A[i];
j--;
}
A[i]=tmp;
quickSort(A,left,i-1);
quickSort(A,i+1,right);
}
}
//然后使用平台算法
数组中存在主元素时,所有的非主元素个数和必少于一半。如果让主元素与一个非主元素"配对“,则最后多出来的元素(没有元素与之配对)就是主元素。从前往后扫描数组元素,假定遇到的当前值选定为主元素,再次遇到它时计数加1,遇到不等的值时,计数减1。当计数减为0后,将遇到的下一个值重新选定为主元素。扫描完毕,当前选定的元素(计数值大于0)可能是主元素,但未必是主元素。还需要对数组再进行一次扫描,记录它出现的实际个数,以判定它是否是主元素。
void mainElemment(int A[],int n)
{
int base=A[0];
int count=1;
int count2=0;
for(int i=1;i<n;i++)
{
if(A[i]==base)
count++;
else
{
if(count>0) count--;
else
{
base=A[i];
count=1;
}
}
}
if(count>0)
{
count2=0;
for(int i=0;i<n;i++)
{
if(base==A[i])
count2++;
}
}
if(count2>(n/2)) return base;
else return -1;
}