一.引用
引用可以被看做是实参的别名,因为引用变量和原变量公用一个地址。对引用变量的操作就相当于对实参变量的操作。(zwh即视感)
举例:
void increment(int& x) //注①
{
x++;
}
int main() {
int a = 1;
int& b = a; // 定义一个a的引用
increment(b); // 传递引用b
std::cout << a << std::endl; // 输出2
return 0;
}
此代码块中:
1.对b的操作,导致了a值的改变,这是引用的性质。
2.用了引用调用,改变的实参的值。
注意:引用类型参数必须显式地使用引用符号(&)标识,否则编译器会将其解释为传值调用,这样就不能实现引用传递的效果。在这个例子中体现为,increase函数的形参是(&x)。
注意中的注意:这个法则对实参并没有要求。具体见下:
void swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
在函数定义中,使用引用类型参数时,需要在参数名前加上“&”符号,以表示这是一个引用类型参数。
在调用函数时,需要传递变量的地址作为参数,而不是变量本身。例如,下面的代码调用了上面的swap()
函数来交换两个整数的值:
int x = 5, y = 10;
swap(x, y);
在函数调用中,x
和y
的地址被传递给swap()
函数,函数内部使用这些地址来交换变量的值。因为使用了引用类型参数,所以函数可以对传递给它的变量做出永久性的改变,而不是仅仅在函数内部做出改变。
需要注意的是,引用类型参数必须显式地使用引用符号,否则编译器会将参数解释为值类型参数。例如,下面的函数定义中,a
和b
都是值类型参数:
void swap(int a, int b)
{
int temp = a;
a = b;
b = temp;
}
int x = 5, y = 10;
swap(x, y);
在调用这个函数时,传递变量的地址是没有意义的,因为函数只是对传递给它的参数做出了临时性的改变,而不是永久性的改变。因此,代码不会交换x
和y
的值。
结果:
#include <iostream>
using namespace std;
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main()
{
int x = 5, y = 10;
swap(x, y);
cout<<x<<endl;
cout<<y<<endl;
return 0;
}
二.函数模板(了解)
一句话:可以对不同类型的数据进行运算,如int和int相加,float和float相加。
例如:
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
int x = 1, y = 2;
float f1 = 1.5, f2 = 2.5;
std::cout << add(x, y) << std::endl; // 输出3
std::cout << add(f1, f2) << std::endl; // 输出4.0
return 0;
}
我们定义了一个函数模板add,用于执行加法运算。函数模板接受两个相同类型的参数,并返回它们的和。在main函数中,我们分别使用int和float类型的参数调用add函数模板,它们被实例化为特定类型的函数,分别计算了两个整数和两个浮点数的和。
三.线性表
(一)·基本语法和定义
表示方式:要有数据对象和数据关系
ADT List
{
数据对象:
D={ai | 0≤i≤n-1,n≥0 }
数据关系:
r={<ai,ai+1> | ai,ai+1∈D,i=0,…,n-2}
基本运算:
CreateList( a):由整数数组a中的全部元素建立线性表的相应存储结构。
Add(e):将元素e添加到线性表末尾。
getlength():求线性表的长度。
GetElem(int i):求线性表中序号为i的元素。
SetElem(int i,T e):设置线性表中序号i的元素值为e。
GetNo(T e):求线性表中第一个值为e的元素的序号。
Insert(int i,T e):在线性表中插入数据元素e作为第i个元素。
Delete(int i):在线性表中删除第i个数据元素。
DispList():输出线性表的所有元素。
}
具体可以参考这个实例:定义一个圆的抽象数据类型
(二).存储结构
1.顺序存储结构
const int initcap=5; //顺序表的初始容量(5)
template <typename T>
class SqList //顺序表类模板
{
public:
T* data; //存放顺序表元素空间的指针
int capacity; //顺序表的容量
int length; //存放顺序表的长度
//线性表的基本运算算法
};
data
:一个指向模板类型T
的数组指针,用于存储顺序表中的元素。capacity
:一个整数类型的属性,表示顺序表的容量(即数组的长度)。length
:一个整数类型的属性,表示顺序表中当前存储的元素个数。
注意T* data,下面具体说明:
1.T
是一个占位符,可以被实际的类型替换,使得该模板可以用于创建不同类型的顺序表。
2.data
被定义为指向模板类型T
的数组指针,是因为顺序表是一种基于数组的数据结构。
顺序表的实现方式是使用一个一维数组来存储所有的元素,因此,data
需要指向一个连续的内存空间,这个空间可以按照数组的方式进行访问。所以,data
需要是一个数组指针,指向一个T
类型的连续内存空间,以便于对顺序表中的元素进行随机访问。
如果data
被定义为其他类型的指针,如单个T
类型的指针,则无法实现对顺序表中的元素进行随机访问,因为顺序表中的元素是按照数组的方式进行存储和访问的。
顺序表通常需要使用数组来存储元素,因此,顺序表类中需要声明一个数组指针来指向存储元素的内存空间。这个数组指针可以指向不同类型的元素,以实现不同类型的顺序表。
需要注意的是,数组指针只是指向一个连续内存空间的指针,它本身并不包含任何数组元素。因此,在使用数组指针之前,需要为其分配内存空间,以便于存储元素。
in short:t可以看成一种数据类型,如int,float等。
2.动态分派顺序表的空间
void recap(int newcap) //改变顺序表的容量为newcap
{ if (newcap<=0) return;
T* olddata=data;
data=new T[newcap]; //分配新空间
capacity=newcap; //更新容量
for(int i=0;i<length;i++) //元素复制
data[i]=olddata[i];
delete [] olddata; //释放原空间
}
若新容量大于0,则:
定义一个数据类型为t的olddata指针,初始化为data指向的地址。
为数组指针data
分配新的内存空间,大小为新容量newcap
,并将其更新为顺序表的新容量。
将原有数据中的元素复制到新的内存空间中。
释放原有数据所在的内存空间。
分析语法:data=new T[newcap]
这里的new
关键字用于创建一个类型为T
的数组,其大小为newcap
。因此,这行代码可以理解为:为顺序表类中的数组指针data
分配一个大小为newcap
的T
类型的数组,并将其地址赋值给指针data
。
【看不懂可以想,data是前面声明过的t类型的指针,newcap是数组的长度,p=new int【10】这样就能懂了】
基础相关:分配内存空间new的语法
new 数据类型;如用 int *p=new int,可以动态分配一个整型变量的内存空间,并将其地址赋值给指针p。释放:delete 指针,如delete p。
若是动态分配数组类型:int *p=new int【10】,释放用delete 【】p。
3.整体建立顺序表
由含若干个元素的数组a的全部元素整体创建顺序表,即依次将a中的元素添加到data数组的末尾,当出现上溢出时按实际元素个数length的两倍扩大容量。
void CreateList(T a[],int n) //由数组a中元素整体建立顺序表
{
for (int i=0;i<n;i++)
{ if (length<capacity) //容量不够时
recap(2*length); //扩大容量
data[length]=a[i];
length++; //添加后元素个数增加1
}
}
注意:data被定义为数组指针,但是c++中数组指针和数组名一样,都是可以用data[i] 来访问数组中的第 i 个元素是符合c++的语法的。
基础相关:数组名和数组指针
数组指针可以用来指针算术运算,如
#include <iostream>
using namespace std;
int main() {
int arr[] = {1, 2, 3, 4, 5};
int *ptr = arr; // 定义一个指向数组 arr 的指针变量 ptr,等价于 int *ptr = &arr[0];
// 使用指针算术运算访问数组中的元素
cout << "arr[0] = " << *ptr << endl; // 输出 arr[0] 的值,即 1
cout << "arr[1] = " << *(ptr + 1) << endl; // 输出 arr[1] 的值,即 2
cout << "arr[2] = " << *(ptr + 2) << endl; // 输出 arr[2] 的值,即 3
// 使用指针算术运算修改数组中的元素
*(ptr + 1) = 20; // 将 arr[1] 的值修改为 20
cout << "arr[1] = " << arr[1] << endl; // 输出 arr[1] 的值,即 20
return 0;
}
4.顺序表的基本运算算法
(1).顺序表的初始化和销毁
构造函数
SqList() //构造函数
{ data=new T[initcap]; //为data分配初始容量大小的空间
capacity=initcap; //初始化容量
length=0; //初始时置length为0
}
初始化复制构造函数(拷贝构造函数)
SqList(const SqList<T>& s) //初始化复制构造函数
{ capacity=s.capacity; //复制容量
length=s.length; //复制长度
data=new T[capacity]; //为当前顺序表分配空间
for (int i=0;i<length;i++) //元素复制
data[i]=s->data[i];
}
做到深拷贝(即复制一份副本)而不是浅拷贝(指针直接赋值给另外一个对象,公用一个存储空间)。拷贝构造函数的语法与普通构造函数类似,但是在函数参数前面加上了 const
关键字和一个引用符号 &
,表示该参数是一个常量引用。注意要进行深度拷贝,即逐个拷贝数组中的元素。
析构函数
~SqList() //析构函数
{
delete [] data; //释放data指向的空间
}
(2).将元素e添加到线性表的末尾Add(e)
void Add(T e) //在线性表的末尾添加一个元素e
{ if (length==capacity) //顺序表空间满时倍增容量
recap(2*length);
data[length]=e; //添加元素e
length++; //长度增1
}
辨析长度和容量:长度是指该线性表中元素的个数,即表中元素个数的计数值。而容量则是指线性表所能容纳元素的最大数量,所以对length*2操作。
(3)求线性表的长度
int Getlength() //求顺序表的长度
{
return length;
}
(4)求线性表中序号为i的元素
bool GetElem(int i, T& e) //求序号i的元素值
{ if (i<0 || i>=length)
return false; //参数错误时返回false
e=data[i]; //取元素值
return true; //成功找到元素时返回true
}
(5)设置线性表中序号为i的元素
bool SetElem(int i,T e) //设置序号i的元素值
{ if (i<=0 || i>=length) //参数错误时返回false
return false;
data[i]=e;
return true;
}
(6)求线性表中第一个值为e的逻辑序号
int GetNo(T e) //查找第一个为e的元素的序号
{ int i=0;
while(i<length && data[i]!=e)
i++; //查找元素e
if (i>=length) //未找到时返回-1
return -1;
else
return i; //找到后返回其序号
}
(7)线性表中插入第i个元素为e
bool Insert(int i, T e) //在线性表中序号i位置插入元素e
{ if (i<0 || i>length) //参数i错误返回false
return false;
if (length==capacity) //满时倍增容量
recap(2*length);
for (int j=length;j>i;j--) //data[i]及后元素后移一个位置
data[j]=data[j-1];
data[i]=e; //插入元素e
length++; //长度增1
return true;
}
(8)在线性表中删除第i的元素
bool Delete(int i) //在线性表中删除序号i的元素
{ if (i<0 || i>=length) //参数i错误返回false
return false;
for(int j=i;j<length-1;j++)
data[j]=data[j+1]; //将data[i]之后元素前移一个位置
length--; //长度减1
if (capacity>initcap && length<=capacity/4)
recap(capacity/2); //满足缩容条件则容量减半
return true;
}
(9)输出所有元素
void DispList() //输出顺序表L中所有元素
{ for (int i=0;i<length;i++) //遍历顺序表中各元素值
cout << data[i] << " ";
cout << endl;
}
程序验证
#include "SqList.cpp" //引用顺序表类
int main()
{ int i,e;
SqList<int> L; //建立类型为int的顺序表对象L
cout << "创建整数顺序表L" << endl;
L.Insert(0,2); //插入元素2
L.Insert(1,3); //插入元素3
L.Insert(2,1); //插入元素1
L.Insert(3,5); //插入元素5
L.Insert(4,4); //插入元素4
L.Insert(5,1); //插入元素1
L.Add(8); //添加整数8
cout << "顺序表L:"; L.DispList();
cout << "长度:" << L.length << " 容量:"
<< L.capacity << endl;
i=3; L.GetElem(i,e);
cout << "序号为" << i << "的元素:" << e << endl;
e=1;
cout << "第一个" << e << "的元素序号=" <<
L.GetNo(e) << "\n";
i=2; cout << "删除序号为" << i << "的元素\n";
L.Delete(i);
cout << "顺序表L:";L.DispList();
cout << "长度:" << L.length << " 容量:"
<< L.capacity << endl;
int b[]={0,1,1,0,1};
for (int i=0;i<5;i++)
{ cout << "删除序号为" << b[i] << "的元素\n";
L.Delete(b[i]);
cout << "顺序表L:";L.DispList();
cout << "长度:" << L.length << " 容量:"
<< L.capacity << endl;
}
cout << "销毁顺序表L" << endl;
return 0;
}
顺序表的应用算法设计示例
一.基于顺序表的算法设计
1•对于含有n个整数元素的顺序表L,设计一个算法将其中所有元素逆置。
•例如L=(1,2,3,4,5),逆置后L=(5,4,3,2,1)。并给出算法的时间复杂度和空间复杂度。
使用双指针的方法。代码中,我们定义了两个指针i和j,分别指向顺序表L的第一个元素和最后一个元素。然后,我们不断交换i和j两个指针所指向的元素,直到它们相遇为止。
void Reverse(SqList<T>& L) //求解算法
{ int i=0,j=L.length-1;
while (i<j)
{ swap(L.data[i], L.data[j]); //序号i和j的两个元素交换
i++; j--;
}
}
算法中的形参"SqList<T>& L",表示的是一个引用类型的参数,它是一个顺序表类型的变量L的引用,即传递了一个顺序表L的引用到函数中,从而函数可以通过该引用来访问和修改顺序表L的元素。
2.假设有一个整数顺序表L,设计一个算法用于删除从序号i开始的k个元素。
例如L=(1,2,3,4,5),删除i=1开始的k=2个元素后L=(1,4,5)。
在参数正确时,直接将ai+k~an-1的所有元素依次前移k个位置。
bool Deletek(SqList<T>& L,int i,int k) //求解算法
{ if (i<0 || k<1 || i+k<1 || i+k>L.length)
return false; //参数i和k错误返回false
for (int j=i+k;j<L.length;j++) //删除k个元素
L.data[j-k]=L.data[j];
L.length-=k; //长度减k
return true;
}
j-k=i,j=i+k;将i+k平移到i的位置。
L.length-=k是简写,就是L.length = L.length - k。
3.对于含有n个整数元素的顺序表L,设计一个算法用于删除其中所有值为x的元素。
例如L=(1,2,1,5,1),若x=1,删除后L=(2,5)。并给出算法的时间复杂度和空间复杂度。
解法1:对于整数顺序表L,删除其中所有x元素后得到的结果顺序表可以与原L共享,所以求解问题转化为新建结果顺序表
template <typename T>
void Deletex1(SqList<T>& L, int x) //求解算法1
{ int k=0;
for (int i=0;i<L.length;i++)
if (L.data[i]!=x) //将不为x的元素插入到data中
{ L.data[k]=L.data[i];
k++;
}
L.length=k; //重置L的长度为k
}
解法2:前移法,对于整数顺序表L,从头开始遍历L,用k累计当前为止值为x的元素个数(初始值为0),处理当前序号为i的元素ai:
(1)若ai是不为x的元素,此时前面有k个为x的元素,将ai前移k个位置,继续处理下一个元素。
(2)若是为x的元素,置k++,继续处理下一个元素。
最后将L的长度减少k。
template <typename T>
void Deletex2(SqList<T>& L, int x) //求解算法2
{ int k=0; //累计等于x的元素个数
for (int i=0;i<L.length;i++)
if (L.data[i]!=x) //将不为x的元素前移k个位置
L.data[i-k]=L.data[i];
else //累计删除的元素个数k
k++;
L.length-=k; //将L的长度减少k
}
二.有序顺序表的算法设计
1.有两个按元素值递增有序的整数顺序表A和B,设计一个算法将顺序表A和B的全部元素合并到一个递增有序顺序表C中。并给出算法的时间复杂度和空间复杂度。
二路归并算法:
template <typename T>
void Merge2(SqList<T> A,SqList<T> B,SqList<T>& C)
{ int i=0,j=0; //i用于遍历A,j用于遍历B
while (i<A.length && j<B.length) //两个表均没有遍历完毕
{ if (A.data[i]<B.data[j])
{ C.Add(A.data[i]); //归并A[i]:将较小的A[i]添加到C中
i++;
}
else //归并B[j]:将较小的B[j]添加到C中
{ C.Add(B.data[j]);
j++;
}
}
while (i<A.length) //若A没有遍历完毕
{ C.Add(A.data[i]); //归并A中剩余元素
i++;
}
while (j<B.length) //若B没有遍历完毕
{ C.Add(B.data[j]); //归并B中剩余元素
j++;
}
}
想创建一个新顺序表,就是SqList<T>& C,声明一个顺序表是SqList<T> A和SqList<T> B。
SqList<T>& C表示创建一个新的顺序表,并将其作为引用类型参数传递给函数。在函数内部,可以通过添加元素等操作来修改C,并将修改后的结果保存在外部的C对象中。
例题:一个长度为L(L≥1)的升序序列S,处在第L/2个位置的数称为S的中位数。
(1)给出算法的基本设计思想。(2)根据设计思想,采用C++描述算法,关键之处给出注释。(3)说明你所设计算法的时间复杂度和空间复杂度。
实际上,不需要求出S的全部元素,用k记录当前归并的元素个数,当k=n时,归并的那个元素就是中位数。
template <typename T>
T Middle(SqList<T> A,SqList<T> B) //求解算法
{ int i=0,j=0; //i,j分别遍历A和B
int k=0; //累计归并的次数
while (i<A.length && j<B.length) //两个有序顺序表均没有遍历完
{ k++; //归并次数增1
if (A.data[i]<B.data[j]) //A中当前元素为较小的元素
{ if (k==A.length) //恰好归并了n次
return A.data[i]; //返回A中的当前元素
i++;
}
else //B中当前元素为较小的元素
{ if (k==B.length) //恰好归并了n次
return B.data[j]; //返回B中的当前元素
j++;
}
}
}
2.3 线性表的链式存储结构
2.3.1 线性表的链式存储结构—链表( 不连续的空间存储)
template <typename T>
struct LinkNode //单链表结点类型
{ T data; //存放数据元素
LinkNode<T>* next; //下一个结点的指针
LinkNode():next(NULL) {} //构造函数
LinkNode(T d):data(d),next(NULL) {} //重载构造函数
};
单链表类模板LinkList:
插入结点操作:
将结点s插入到单链表中p结点的后面。
s->next=p->next;
p->next=s;
删除结点操作:
删除单链表中p结点的后继结点。
q=p->next; //q指向被删结点
p->next=q->next; //从单链表中删除结点q
delete q; //释放空间
整体建立单链表
头插法建表
s->next=head->next;
head->next=s;
void CreateListF(T a[],int n) //头插法建立单链表
{ for (int i=0;i<n;i++) //循环建立数据结点
{ LinkNode<T>* s=new LinkNode<T>(a[i]);//创建数据结点s
s->next=head->next; //将结点s插入head结点之后
head->next=s;
}
}
这段代码中,使用 new 运算符创建了一个 LinkNode<T> 类型的新节点对象。括号中传入了一个参数 a[i],表示节点中存储的数据值。这个参数的类型应该与模板参数 T 的类型相匹配。
new 运算符返回一个指向新节点对象的指针,这个指针的类型为 LinkNode<T>*。这个指针被赋值给变量 s,这样变量 s 就指向了新创建的节点对象。