#include<iostream>
#include<vector>
#include<cassert>
#include<Array>
using namespace std;
/*
* 基本数据类型是由c++系统预编译的,而自定义类型的数据是由多个基本类型或者自定义类型的元素组成,称之为群体数据,对于这些数据,仅有的系统预定义的操作是远远不够的,在很多情况下,需要设计与某些具体问题相关的特殊操作,并按照面向对象的方法将数据和操作封装起来
* 这就是群体类,群体类分为两种:线性和非线性群体,线性群体中的元素按照位置排列有序,可以分为第一个元素第二个元素,一维数组用下标映射顺序,是线性群体的典型例子
* 非线性群体不用位置顺序来标识元素,像上下层关系、族谱等都是非线性群体
* 两种常用的算法:排序和查找方法
* 排序:又称为分类或整理,一个将无需序列调整为有序序列的的过程,在排序中需要完成两个基本操作:1、比较数字的大小2、调整元素在序列中的位置
* 查找:在一个序列中按照某种方式找到需要的特定数据元素的过程
* 一、函数模板与类模板
* 模板是c++支持参数化程序设计的工具,通过它可以可以实现参数多态性,所谓参数化多态性,就是将程序所处理的对象的类型参数化,使一段程序可以用于处理不同类型的对象
* (一)函数模板
* 一个算法处理不同类型的的数据类型,即使设计同样的函数名也只是函数重载,函数体仍然要分别定义
* 使用函数模板,只需要程序员对于函数模板进行一次编写,然后基于调用函数时的参数类型,C++编译器就会自动产生相应的函数来调动它
* 函数模板的定义:
* template<模板参数列表>//class /typename标识符,指明可以接收一个类型参数
* 类型名 函数名(参数表)
* {函数体}
* 当类型参数的含义确定后,编译器以函数模板为一个样例,生成一个函数,这一过程称为函数模板的实例化,生成的函数称为函数模板的一个实例
* 从模板函数上产生的相关函数都是同名的,编译器用重载的方式进行调用,函数模板本身也可以有多种方式重载
* 函数模板和函数的区别:
* 1、 函数模板在编译时不会生成任何目标代码,只有模板生成的实例会生成目标代码
* 2、被多个源文件引用的函数模板,应该连同函数体一起放在头文件中而不是像普通函数那样,只将声明放在头文件中
* 3、函数指针只能指向模板的实例,而不能指向模板本身
* (二)类模板
* 使用类模板使用户可以为类定义的一种模式,使得类中的某些数据成员、函数成员、返回值、或局部变量能取任意值(包括系统预定义的和用户自定义的)
* 类使对一组对象的公共性质的抽象,类模板是对于不同类的公共性质的抽象,所以类模板属于更高层的抽象,类模板也称为参数化类
* 类模板声明:
* template<模板参数表>
* class 类名
* {类成员声明}
* 类成员的声明和普通类的定义几乎完全相同,只是在它的各个成员中通常要用到模板定义的类型参数T,其中模板参数表的形式和函数模板中的参数表的形式完全一样
* 如果需要在类模板以外定义其成员函数,采用以下形式:
* template<模板参数表>
* 类型名 类名<模板参数标识符列表>::函数名(参数表)
* 一个类模板声明自身并不是一个类,它说明的是类的一个家族,只有它被其他代码引用时,模板才会根据引用需要生成具体的类
* 使用类模板建立一个对象:
* 模板名 <模板参数表>对象名1,名2……;
* 二、线性群体
* (一)线性群体的概念
* 线性群体中的元素次序与其位置关系是相对应的
* 对可直接访问的线性群体,我们可以直接访问群体中的任何一个元素,而不用访问该元素之前的元素,人、例如:我们可以通过数组下标取访问数组中的任何元素
* 对于顺序访问的线性群体,只能按照元素访问的线性群体,只能按照元素的顺序,从头开始依次访问各个元素
* 有两种特殊的线性群体:栈和队列
* 栈:
* 栈是一种只能从一端访问的线性群体,可以访问的一端称为栈顶,另外一端称为栈底,对栈顶位置的标记称为栈顶指针,对于栈底位置的标记称为栈底指针,向栈顶添加元素称为压入栈,删除栈顶元素称为弹出栈,栈中的元素添加和删除操作具有后进先出
* 编译系统就是通过栈来实现函数调用时参数的传递和保留返回地址,编译器用高级语言编写程序时也可以用栈来实现
* 队列:
* 队列也是一种特殊的线性群体,队列是只能从一段添加元素,从另外一段删除元素的线性群体,可以添加元素的一端称为队尾,可以删除元素的一端为队头
* 对对头位置的标记称为队头指针,对队尾的标记称为队尾指针,向队列添加元素称为入队,删除对头元素称为出队
* (二)直接访问群体——数组类
* 静态数组是具有固定元素个数的群体,其中的元素可以直接通过下标来访问,其大小在编译时就已经确定,无法修改
* 1、浅复制、深复制
* 浅复制是由默认构造函数实现的,将对应数据成员一一赋值
* 浅拷贝:指向同一个内存空间
* 深拷贝:拷贝一个新副本,对副本进行操作,原来的不会收到影响
* 2、与众不同的运算符
* 重载的运算符,使用的都是对象的引用,如果一个函数的返回值是对象的值,他不应该称为左值
* []恰恰常常当作左值,可以将[]重载函数的返回值指定为引用,由于引用的是对象的别名,所以通过引用也可以改变函数的值;
* =在赋值语句表达式也可以作为左值((a=b)++)所以,重载函数的返回值类型也指定为引用
* 一般,当对象需要执行显性定义的深复制时,也需要重载赋值运算符,如果不重载,系统会自动生成一个隐含的重载函数,该函数会分别对每一个数据成员执行=操作
* =[]()->只能被重载为成员函数,而且派生类的=总是会隐藏基类de=
* 3、指针转换运算符的作用
* 如果想将自定义的类型T显性或者隐含的转换为S类,可以有operator S定义为自定义类S的成员函数,或者用ststic_cast显性的转换为S类型,这样函数成员会被调用,不用指定返回值类型,返回值类型会和操作符名称一致,不能为这类函数规定返回值(void也不行)
* 当对象本身是常数时,为了避免通过指针对于数组的内容进行修改,只能将对象转换为常函数
* (三)顺序访问群体——链表
* 链表是一种动态数据结构,可以用来表示顺序访问的线性群体,链表由一系列结点组成,结点可以在运行时动态生成,每一个结点包含数据域和指向链表中下一个结点的指针(下一个结点的地址)
* 指针是维系结点的纽带,节点中可以不止有一个用于连接其他节点的指针,如果每个结点中只有一个指向后继节点的指针,则称为单链表,如果每个结点有两个指针,一个指向前驱节点,一个指向后继节点,那么称为双链表
* 链表的第一个结点称为头节点,最后一个结点称为尾结点,尾结点的后继指针为空
* 1、节点类
* 链表的结点包括数据和指针域,是链表的基本构件,结点的数据域用于存放群体中元素的内容,既可以是若干个基本类型的数据,也可以是自定义类型的数据,甚至可以是内嵌对象,结点的指针域用于存放指向下一个结点的指针(下一个结点的地址)
* 节点类的数据成员应该包括数据域和指针域的内容,函数成员中应该含有对于数据和指针初始化的方法(函数)
* 以及在本结点之后插入新节点和删除后继节点的方法
* 2、链表类
* 建立链表和遍历链表(逐个访问链表的每一个结点)以及插入和删除结点是应用程序中广泛使用的基本链表算法
* 将结点类和操作封装起来构成了链表类
* (1)链表类的数据成员
* 链表类表示的是一个顺序访问饿线性群体,由一组用指针域串联的Node模板对下个构成,对于链表的任何操作都是由头节点开始,因此对于所有应用链表的程序来说,链表的头指针是必须使用的,尾指针对于很多应用来说也是有用的信息
* 由于链表是动态数据结构,其结点个数是动态变化的,因此需要封装在类中进行实时维护
* 在对链表的顺序访问中,往往需要一个指针,从头节点开始依次遍历各个结点,直到所需要访问的位置,这个指针所指向的结点称为当前结点,另外还需要一个伴随指针,始终指向该结点的前驱节点,以配合完成结点中的插入删除等操作
* 所以链表的数据成员应该保存,表头指针,表尾指针,元素个数,当前遍历位置等信息
* (2)链表的函数成员
* 链表的基本操作应该包括:生成新结点、插入新结点、删除结点,访问、修改结点数据、遍历链表等还有一些辅助函数,为了方便链表之间对象的赋值还应该重载=运算符,另外还需要提供一些接口函数
* 3、栈类
* 栈是一种特殊的线性群体,栈的数据可以通过数组和链表来存取,但是对栈中的元素饿访问是受控制的,压入栈和弹出栈,都只能在栈顶进行,因此不适合用数组类和链表类解决问题,而是需要设计栈类
* 要完整的保存栈的信息,栈的数据成员至少应该包括栈元素和栈顶指针,因为栈元素既可以用数组保存又可以用链表保存
* 栈类的结构有两种,基于数组和基于链表,用数组来存储元素,不仅可以使用静态数组还可以使用vector或者Array
* 栈的基本状态:一般状态、栈空、栈满
* 当栈中没有元素称为空栈,当栈中的元素达到上限时,称为满栈,当元素中有栈但是没有达到上限时,就是一般状态
* 用静态数组存储栈元素,达到声明的元素个数就是满栈,使用动态数组或者链表可以根据需要设定或者不设置元素的最大个数
* 无论是哪一种数据结构,栈类都应该有以下基本操作:初始化、入栈、出栈、栈清空、访问栈顶元素、检测栈状态
* push(const T&a)//将a压入栈
* pop()//将栈顶元素弹出
* clear()//将栈清空
* peep()//访问栈顶元素
* isEmpty()//测试栈是不是满的
* isFull()//测试栈是不是空的
* 4、队列类
* 队列也有三种状态:一般状态、队满、队空
* 队列类的数据成员抖应该包括:队列元素、队头指针、队尾指针,函数成员应该包括:初始化、入队、出队、清空队列、访问队首元素、检测队列状态
* 三、群体数据的组织
* (一)插入排序
* 每一步将待排元素按照大小插入到已经拍好的序列的合适位置上
* (二)选择排序
* 每一次从里面取出最小的数据放在已排序列的后面
* (三)交换排序
* 两两比较待排序序列中的元素,并交换不满足序列要求的各个元素,直到全部满足要求为止,最简单的交换排序是冒泡排序
* (四)顺序查找
* 从元素的首地址开始,将元素与待查找的逐个比较,直到找到相等的为止,如果整个数组都没有相等的则查找不成功
* (五)折半查找
* 对于已经按照关键字排序之后的序列,经过一次比较后,将序列分割为两个部分,逐步缩小查找范围,直到找到或者找不到为止
* 四、深度探讨
* (一)模板的实例化机制
* 1、对模板与模板实例关系的辨析
* 模板类本身并不是一个类,并不能代表一种数据类型,只有模板的实例(书写为带参数的类模板)才能够当作类来使用
* 一切需要使用类型名称的地方,如果需要使用模板类要为他提供一定的参数,使得被引用的是模板类的实例,这些参数中可以包括未被定义的参数
* 同一个模板类根据不同的参数生成的实例,是两个完全无关的类型,无法为对方的对象初始化或者赋值,两种类型的指针变量也是不兼容的,也无法通过自身的对象去调用别人的成员函数
* 函数模板也并不是函数,编译器不会为函数模板本身生成目标代码,在调用一个函数模板时,虽然用的是模板名,但是,实际上调用的是函数模板的实例
* 2、隐含实例化
* 编译器根据函数模板生成的函数,或根据类模板生成具体的类的过程叫模板的实例化
* 函数模板的实例化是按照需要进行的,只有被调用的函数模板才会被实例化
* 类模板在进行实例化时,只有他的成员的声明会被实例化,对于类模板的函数成员的实例化也是按照需要进行实例化的,只有一个函数被使用时,他才会实例化
* 3、多文件结构中模板的组织
* 函数模板、类模板成员函数、类模板静态数据成员,不能像普通函数、普通的成员函数和普通的静态数据成员那样把定义放在源文件中,而把声明放在头文件中,应该将他们的定义也都放在头文件中
* 如果放在源文件中,引用头文件,可以进行实例化类,但是没有办法对于类的成员函数进行实例化,因为编译系统对于每一个源文件都是分别编译的
* 4、显式实例化
* 隐含实例化是按照需求进行的,而显式是有专门的代码指定的
* template 实例化目标声明
* template void fun<int>(int a,int b)//对于函数fun实例化为int类型
* template void fun(int a,int b)//可以根据参数列表自行推导出来
* template class A<double>//实例化一个模板类为double类型
* (二)为模板定义特殊的实现
* 模板的好处在于使得一个数据结构或者算法的程序只需要写一次,就能够适用于各个数据类型,具有普遍适用性,但是不能够根据某些数据类型的特殊性,设计出的模板对于具体的数据类型而言未必具有最好的效率
* 1、模板的特化
* 程序员为一个函数模板或雷米版在某些特定参数下提供的特殊定义,叫做模板的特化
* template<>//是进行特化时需要的固定格式,将类模板特化后,还需要给出特化的类模板的成员函数的定义
* 2、模板类的偏特化
* 一部分参数固定,而另一部分参数可变的情况下规定类模板的特殊实现,这种行为称为类模板的偏特化
* 3、函数模板的重载
* 函数模板不能偏特化,但是函数模板可以像普通参数那样重载,通过将函数模板重载也可以完成与类模板偏特化类似的功能
* (三)模板元编程简介
* 就是在模板实例化的同时利用编译器完成一些计算任务,从而提升程序的运行时效率
* 静态数组的大小往往由常量来决定,但是常量的简单表达式并不能表示所需的数组大小
* 这个时候可以借助元编程
* template<unsigned N>
* struct fun{
* enum{v=n*fun(n-1)::v}
}
template<>
struct fun<0>
{
enum{v=1}//类的特化,设定n为0时,N的阶乘diji*
*
*
*
*/
#if 0
//模板类定义
template<class T>
//template<typename T>
T abs(T a)
{
return a > 0 ? a : -a;
}
int main()
{
int x = 1;
double b = -1.01;
cout<<abs(x)<<endl;
cout<<abs(b);
}
//函数模板
template<typename T>
void show(int a, T &arr)//定义模板类
{
for (int i = 0; i < a; i++)
{
cout << arr[i];
}
cout << endl;
}
int main()
{
const int x = 8, y = 8, z = 20;
int a[x] = { 1,2,3,4,5,6,7,8 };
double b[y] = { 1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8 };
char c[z] = "welcome to you!";
show(x, a);//int类型数组进行调用
show(y, b);//double类型数组进行调用
show(z, c);//字符串进行调用
return 0;
}
//类模板
template <class T>
class A
{
public:
A() {}
void show(T a);
private:
T x;
};
template <class T>
void A<T>::show(T a)//在类模板外定义函数成员;类型名 类名<模板参数列表>::函数名(形参表)
{
x = a;
cout << x<<endl;
}
int main()
{
int* p = new int(1);
cout << *p<<endl;
int* p1 = new int[10]();//new定义动态数组:new 类型名[数组长度]();
for (int i = 0; i < 10; i++)
{
cout << *p1;
p1++;
}
vector<int>arr(10);
//vector定义动态数组vector <类型名>数组名(数组长度)
cout << endl;
A<int> a;
a.show(1);
A<double>b;
b.show(1.7);
return 0;
}
//浅复制和深复制
//浅拷贝就是用复制构造函数实现的
template <class T>
class A
{
public:
A(Array <T>a)
{
arr = a;
}
A(Array<T>& p)
{
size = p.size;;
list = a.list;
}
private:
Array <T>arr;
};
int main()
{
A<int> a(10);
A<int>b(a);
//A<int>b(a);
}
//数组并没有被真的复制,只是将元素首地址赋值给了复制的指针,并没有构成一段新的数组,只是两者共用一个内存皴法数组元素
//并且在执行析构函数的时候,会调用两次,会造成对于同一个内存的两次释放,不允许,所以应该使用深复制
//深复制
template<class T>
Array<T>::Array(Array<T>& a)
{
size = a.size//获取a的数组大小,赋给当前对象的成员
list = new T[size];//new申请空间 new 类型名[数组长度]//new 类型名(初始化);
//为对象申请内存,并进行检查,
for (int i = 0; i < size; i++)
{
list[i] = a.list[i];
//将a的值一一复制给新的对象
}
}
//重新建立一个对象,并且申请空间,复制原对象的所有数据,两个对象相互独立,互不影响,调用析构函数也不会进行二次删除
//形参不会影响实参的值
int fun(int x)
{
x = 2 * x;
return x;
}
int main()
{
int a = 1;
cout << fun(a) << endl;
a = fun(a);
cout<<a<<endl;
}
void read(int* p, int n)
{
for (int i = 0; i < n; i++)
{
cin >> p[i];
}
}
void show(int*p,int n)
{
for (int i = 0; i < n; i++)
{
cout<< p[i];
}
}
int main()
{
int a[10] = { 1 };
//vector <int>a(10);//这时的A是一个对象名,不是int型
//编译器无法对他进行转换
read(a, 10);
show(a, 10);
return 0;
}
//求2~n中的质数,n由键盘输入
void zhishu(vector<int>&a, int n)
{
for (int i = 2; i <=n; i++)
{
for (int j = 2; j < i; j++)
{
if (i % j == 0)
{
break;
}
else
{
cout << i;
break;
}
}
}
}
void read(vector<int>& a, int &n)
{
for (int i = 0; i <n-1; i++)
{
a[i]=i+2;
}
}
void show(vector<int>& a, int& n)
{
for (int i = 0; i < n-1; i++)
{
cout << a[i];
}
cout << endl;
}
int main()
{
int n;
int m=1 ;
cin >> n;
vector<int>arr(n);
read(arr, n);
show(arr, n);
zhishu(arr, n);
}
//int类型的链表
class Node1
{
public:
int x;
Node1(Node1* p1, int a) {}
void insert(Node1* p) {}
Node1* deletenode() {}
Node1* nextnode() {}
private:
Node1* next;
};
Node1::Node1(Node1* p1, int a) :next(p1), x(a) {}
void Node1::insert(Node1* p)
{
p->next = next;//p的指针指向下一个的地址
next = p;//让本来的下一个变成p
}
Node1* Node1::deletenode()
{
Node1* ptr=next;
if (next == 0)
{
return 0;
}
next = ptr->next;
return ptr;
}
Node1*Node1:: nextnode()
{
return next;
}
//结点类模板
template<class T>
class Node
{
public:
T date;//数据域
Node(T& date, Node<T>* next = 0) {}
void insert(Node<T>* p);//插入一个结点p;
Node<T>* deleteNode();//
Node<T>* nextNode();
const Node<T>* nextNode()const;
private:
Node<T>* next;//指向下一个结点的指针
};
template<class T>
void Node<T>::insert(Node<T>* p)
{
p = p->next;//p结点的指针域指向当前结点的后继节点
next = p;//当前节点的后继节点指向p
}
template<class T>
Node<T>* Node<T>::deleteNode()
{
Node<T>* ptr = next;
next = ptr->next;
return ptr;
}
template<class T>
Node<T>* Node<T>::nextNode()
{
return next;
}
//插入排序
int main()
{
int arr[] = { 3,2,4,8,7,5,9,0,1,10 };
for (int i = 1; i < 10; i++)
{
int tmp = arr[i];//要使用中间量去接收arr[i]的值
int j = i - 1;
while (j >= 0 && tmp < arr[j])
{
arr[j + 1] = arr[j];//如果不接收,本身arr[i]的值发生改变
j--;
}
arr[j+1] = tmp;
}
for (int i = 0; i < 10;i++)
{
cout << arr[i]<<endl;
}
}
//选择排序
int main()
{
int arr[] = { 3,2,4,8,7,5,9,0,1,10 };
int tmp;
for (int i = 0; i < 10; i++)
{
int tmp1;
tmp = i;
for (int j = i + 1; j < 10; j++)//在已经排序的后面进行选择,再排序
{
if (arr[j] < arr[tmp])
{
tmp = j;
}
}
tmp1 = arr[i];
arr[i] = arr[tmp];
arr[tmp] = tmp1;
cout << tmp;
}
cout << endl;
for (int i = 0; i < 10; i++)
{
cout << arr[i];
}
return 0;
}
//冒泡排序
void jiaohuan(int &a,int &b)
{
int tmp;
tmp = a;
a = b;
b = tmp;
}
int main()
{
int arr[] = { 3,2,4,8,7,5,9,0,1,10 };
int j = 9;
while (j > 0)//从后往前依次确定,用j下标来标记已经排好的下标
{
for (int i = 0; i < j; i++)//每次从下标为0开始,一直到已经排好的j下标
{
if (arr[i] > arr[i + 1])//进行交换
{
jiaohuan(arr[i], arr[i + 1]);
}
}
j--;
}
for (int i = 0; i < 10; i++)
{
cout << arr[i];
}
return 0;
}
//顺序查找
template <class T>
int chazhao(T arr[], int n, T key)
{
for (int i = 0; i < n; i++)
{
if (arr[i] == key)
{
return i;
}
}
return -1;
}
int main()
{
int arr[] = { 3,2,4,8,7,5,9,0,1,10 };
int a = 0;
int n = 10;
int i=chazhao<int>(arr, n, a);
double brr[] = { 2.2,5.5,3.3,4.4,8.8 };
double a1 = 8.8;
int n1 = 5;
int j = chazhao<double>(brr, n1, a1);
cout << i<<j;
return 0;
}
//折半查找
//先排序
int main()
{
int arr[] = { 3,2,4,8,7,5,9,0,1,10 };
int tmp;
for (int i = 0; i < 10; i++)
{
int tmp1;
tmp = i;
for (int j = i + 1; j < 10; j++)//在已经排序的后面进行选择,再排序
{
if (arr[j] < arr[tmp])
{
tmp = j;
}
}
tmp1 = arr[i];
arr[i] = arr[tmp];
arr[tmp] = tmp1;
cout << tmp;
}
cout << endl;
for (int i = 0; i < 10; i++)
{
cout << arr[i];
}
cout << endl;
//再用折半查找
int a = 10;//要查找的数
int low = 0;
int high = 9;
int i=0;
while (low <= high)//判断是否查找完全部的元素
{
int mid = (low + high) / 2;//确定中间数
if (arr[mid] == a)
{
cout << mid;
i = 1;
break;//如果相等,输出下标,并且跳出循环
}
else if (arr[mid] > a)
{
high = mid-1 ;//让末尾下标等于中间值的前一个下标
}
else if (arr[mid] < a)
{
low = mid+1 ;//让头下标等于中间下标的后一个下标
}
}
if (i == 0)
{
cout << -1;
}
return 0;
}
#endif