数据结构回溯

本文介绍了C++中的引用概念,如何通过引用实现对实参变量的直接操作。接着讲解了函数模板,允许对不同类型的数据执行相同运算。然后详细阐述了线性表的定义、存储结构,包括顺序存储和动态扩容,以及线性表的基本运算算法,如添加、删除、查找等。最后提到了链式存储结构中的单链表及其操作。
摘要由CSDN通过智能技术生成

一.引用

引用可以被看做是实参的别名,因为引用变量和原变量公用一个地址。对引用变量的操作就相当于对实参变量的操作。(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);

在函数调用中,xy的地址被传递给swap()函数,函数内部使用这些地址来交换变量的值。因为使用了引用类型参数,所以函数可以对传递给它的变量做出永久性的改变,而不是仅仅在函数内部做出改变。

需要注意的是,引用类型参数必须显式地使用引用符号,否则编译器会将参数解释为值类型参数。例如,下面的函数定义中,ab都是值类型参数:

void swap(int a, int b) 
{
  int temp = a;
  a = b;
  b = temp;
}
int x = 5, y = 10;
swap(x, y);

在调用这个函数时,传递变量的地址是没有意义的,因为函数只是对传递给它的参数做出了临时性的改变,而不是永久性的改变。因此,代码不会交换xy的值。

结果:

#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分配一个大小为newcapT类型的数组,并将其地址赋值给指针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=(12345),逆置后L=(54321)。并给出算法的时间复杂度和空间复杂度。

使用双指针的方法。代码中,我们定义了两个指针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=12345),删除i=1开始的k=2个元素后L=145)。

在参数正确时,直接将ai+kan-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=(12151),若x=1,删除后L=(25)。并给出算法的时间复杂度和空间复杂度。

解法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.有两个按元素值递增有序的整数顺序表AB,设计一个算法将顺序表AB的全部元素合并到一个递增有序顺序表C中。并给出算法的时间复杂度和空间复杂度。

二路归并算法:

 

i 遍历 A j 遍历 B ,均从 0 开始
while i , j 都没有超界
a i b j 比较:较小元素添加到 C 中,后移相应指针
将没有遍历完的元素添加到 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对象中。

算法中尽管有多个 while 循环语句,但恰好对顺序表 A B 中每个元素均访问一次,所以时间复杂度为 O( n + m )
算法中需要在临时顺序表 C 中添加 n + m 个元素,所以算法的空间复杂度也是 O( n + m )

例题:一个长度为LL≥1)的升序序列S,处在第L/2个位置的数称为S中位数

例如:若序列 S 1 =(11 13 15 17 19) ,则 S1 的中位数是 15
两个序列的中位数是含它们所有元素的升序序列的中位数。例如,若 S 2 =(2 4 6 8 20) ,则 S 1 S 2 的中位数是 11
现有两个等长的升序序列 A B ,试设计一个在时间和空间两方面都尽可能高效的算法,找出 两个序列 A B 的中位数 。要求:

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;		//释放空间

整体建立单链表

通过一个含有 n 个元素的 a 数组来建立单链表。
建立单链表的常用方法有两种 头插法 尾插法

头插法建表

从一个空表开始,依次读取数组 a 中的元素
生成新结点 s ,将读取的数据存放到新结点的数据成员中
将新结点 s 插入到当前链表的 表头 上。
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 就指向了新创建的节点对象。

尾插法建表

从一个空表开始,依次读取数组 a 中的元素
生成新结点 s ,将读取的数据存放到新结点的数据成员中
将新结点 s 插入到当前链表的 上。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值