This slide is based on Mark Allen Weiss's book.
中译本是《数据结构与算法分析 C++ 描述》张怀勇等译
数据结构是计算机专业的一门重要的基础课, 在计算机科学各领域尤其是在软件设计和开发中发挥着举足轻重的作用.
几乎所有的计算机软件系统, 例如, 操作系统、编辑工具和编译器等都要使用不同的数据结构.
因此, 数据结构是计算机专业的核心课程, 是许多其他后续课程的重要基础.
数据结构
我们可以将数据结构看作工具箱中的一套工具.
本课程使用C++语言来描述数据结构和算法分析, 因此假设读者具有 C++ 的基础知识. 这方面的参考文献有
该书由 C++ 的创始人 Bjame Stroustrup 所编写, 描述了 C++ 的最新设计标准, 具有最高的权威性.
另一本标准的参考文献是
这两本书都有中译本. 另外钱能写的《C++程序设计教程》(第二版)也不错.
关于C++的高级编程可以参考
另外 S. Meyers 写的以下两本书详尽地讨论了 C++ 的许多缺陷.
中译本名称是《数据结构与算法分析——C++描述》(第3版), 张怀勇 等译.
面向对象编程(
结构与数组类似, 因为结构也可以用来存储数据的多个元素, 但是, 在数组中所有元素的类型都必须是相同的类型.
#include <iostream>
#include <iomanip>
#include <string>
using namespace std;
// 结构名通常使用大写
struct CarType {
string maker;// 结构的元素通常称为数据成员(data member)或简称为成员.
int year;
float price;
};//别忘了分号
void getYourCar (CarType & car);
int main()
{
// CarType 是一个自定义类型
CarType myCar, yourCar;// 变量 myCar, yourCar 使用结构类型声明时, 就生成了对象(object)
//使用同一个结构可以生成一个或多个对象.
myCar.maker = "Mercedes"; // I wish
myCar.year = 2005;// 结构成员的访问, 使用 . 操作符
myCar.price = 45567.75;
getYourCar (yourCar);
cout << "Your car is a: " << yourCar.maker << endl;
cout << fixed << showpoint << setprecision(2) <<
"I'll offer $" << yourCar.price -100 << " for your car."
<< endl;
return 0;
}
void getYourCar (CarType & car)
{
cout << "Enter your maker: ";
cin >> car.maker;
cout << "Enter the year: ";
cin >> car.year;
cout << "Enter the price: $";
cin >> car.price;
}
对象数组
CarType dealerCar[100];
结构中声明的成员可以是数组, 也可以是其他结构.
struct EngineType {
int numCylinders; // 气缸数
float numLiters; // 升
string countryMade;
};
struct CarType {
string maker;
float price;
string wheel[4];
EngineType engine; // 前面一定要先定义 EngineType
};
赋值
myCar.engine.numCylinders = 6; myCar.wheel[2] = "Goodyear";
结构的对象可以赋值给同一结构的另一个对象.
yourCar = myCar;
这个代码将 myCar 的每个成员的值赋给 yourCar 中相应的成员.
using System; // 通用基础类库类型
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace getCarType
{
public struct CarType
{
public string maker;
public int year;
public float price;
public void getYourCar()
{
Console.Write("Enter your maker: ");
maker = Console.ReadLine();
Console.Write("Enter the year: ");
if (int.TryParse(Console.ReadLine(), out year))
{
}
else {
year = 2000;
}
Console.Write("Enter the price: $");
if (float.TryParse(Console.ReadLine(), out price))
{
}
else {
price = 0F;
}
}
public void printTheCar()
{
Console.WriteLine("The info of the car is:");
Console.WriteLine("Maker:" + maker);
Console.WriteLine("Year:" + year);
Console.WriteLine("Price:" + price);
}
}
class Program
{
static void Main(string[] args)
{
CarType myCar;
CarType yourCar;
myCar.maker = "Mercedes";
myCar.year = 2005;
myCar.price = 45567.75F;
myCar.printTheCar();
yourCar = myCar;
yourCar.getYourCar();
yourCar.printTheCar();
Console.WriteLine("Press any key to terminate this program.");
Console.ReadKey();
}
}
}
以下是一些常用的函数:
Reference:
http://www.cppblog.com/masiyou/archive/2009/10/06/97948.aspx
对象内部的数据只能由对象本身访问, 我们称这一特性为
#include <iostream>
using namespace std;
/**
* A class for simulating an integer memory cell.
*/
class IntCell
{
public:
/**
* Construct the IntCell.
* Initial value is 0.
*/
IntCell( )
{ storedValue = 0; }
/**
* Construct the IntCell.
* Initial value is initialValue.
*/
IntCell( int initialValue )
{ storedValue = initialValue; }
/**
* Return the stored value.
*/
int read( )
{ return storedValue; }
/**
* Change the stored value to x.
*/
void write( int x )
{ storedValue = x; }
private:
int storedValue;
};
int main( )
{
IntCell m;
m.write( 5 );
cout << "Cell contents: " << m.read( ) << endl;
return 0;
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IntCell
{
class IntCell
{
// member variables
int storedValue;
public IntCell()
{
storedValue = 0;
}
IntCell(int initialValue)
{
storedValue = initialValue;
}
public int read()
{
return storedValue;
}
public void write(int x)
{
storedValue = x;
}
}
class Program
{
static void Main(string[] args)
{
//IntCell m;//声明, 但未实例化. C#是有保护机制,在使用变量的时候必须赋值。
IntCell m = new IntCell();
m.write(5);
Console.WriteLine("Cell contents: " + m.read());
Console.WriteLine("\nPress any key to terminate this program.");
Console.ReadKey();
}
}
}
//File: MemoryCell.java
/*
// MemoryCell class
// Object read() --> Returns the stored value
// void write(Object x) --> x is stored
*/
public class MemoryCell
{
//public methods
public Object read()
{
return storedValue;
}
public void write(Object x)
{
storedValue=x;
}
//Private internal data representation
private Object storedValue;
}
//TestMemoryCell.java
/*
//Will use the class MemoryCell
*/
public class TestMemoryCell
{
public static void main(String[] args)
{
MemoryCell m = new MemoryCell();
m.write("37");
String val = (String) m.read();
System.out.println("Contents are: "+val);
}
}
注:友元函数包括3种:
Reference:
http://www.cnblogs.com/york-hust/archive/2012/06/01/2530799.html
构造函数是描述如何构建
#include <iostream>
using namespace std;
/**
* A class for simulating an integer memory cell.
*/
class IntCell
{
public:
explicit IntCell( int initialValue = 0 )
: storedValue( initialValue ) { } //初始化列表
int read( ) const //加 const 表示 read() 是访问函数
{ return storedValue; }
void write( int x )
{ storedValue = x; }
private:
int storedValue;
};
int main( )
{
IntCell m;
m.write( 5 );
cout << "Cell contents: " << m.read( ) << endl;
return 0;
}
这里仍然定义了两个名为
默认参数可以在任何函数中使用, 但是最普遍的情况是用在构造函数中.
构造函数在其函数体之前使用初始化列表. 初始化列表用来直接初始化数据成员, 用冒号
在数据成员是具有复杂初始化过程的
IntCell obj; //obj is an IntCell obj = 37; //Should not compile: type mismatch obj.write(37); //work
如果未指定
obj = 37; //编译器试图将此转换为
IntCell temporary = 37; obj = temporary;
在 C++ 中, 每个成员函数都被标记为访问函数或修改函数. 在设计阶段这是很重要的一步, 不可以被简单地看成注释.
C++ 程序通常将类的说明和实现分别放在两个文件中. 所谓接口就是指类的说明.
/**
* 在一个复杂的项目中, 往往包含很多文件. 有的文件包含其他文件.
* 下面的预处理命令避免了文件内容被重复读取的危险.
* 一般定义的符号与文件名基本相一致.
* 条件指示符 #ifndef 检查 IntCell_H 在前面是否已经被定义
*/
#ifndef IntCell_H
#define IntCell_H
/**
* A class for simulating an integer memory cell.
*/
class IntCell
{
public:
explicit IntCell( int initialValue = 0 );
int read( ) const;
void write( int x );
private:
int storedValue;
};
#endif
#include "IntCell.h"
/**
* Construct the IntCell with initialValue
* 成员函数必须声明为类的一部分, 否则函数将会被认为是全局的.
* 语法是 ClassName::member
* :: 称为是作用域运算符.
*/
IntCell::IntCell( int initialValue ) : storedValue( initialValue )
{
}
/**
* Return the stored value.
*/
int IntCell::read( ) const
{
return storedValue;
}
/**
* Store x.
*/
void IntCell::write( int x )
{
storedValue = x;
}
#include <iostream>
#include "IntCell.h"
using namespace std;
int main( )
{
IntCell m; // Or, IntCell m( 0 ); but not IntCell m( );
m.write( 5 );
cout << "Cell contents: " << m.read( ) << endl;
return 0;
}
其他要注意的是:
IntCell obj1; // zero parameter constructor (OK) IntCell obj2(12); // one parameter constructor (OK) IntCell obj3 = 37; // constructor is explicit (WRONG) IntCell obj4(); // This is a function declaration. (WRONG)
C++ 标准定义了两个类
/**
* 创建一个存储100个平方值的向量, 并且将这些数据输出.
*/
#include <iostream>
#include <vector>
using namespace std;
int main( )
{
vector<int> squares( 100 );//这里squares向量中每个元素初始化为0
for( int i = 0; i < squares.size( ); i++ )
squares[ i ] = i * i;
for( int i = 0; i < squares.size( ); i++ )
cout << i << " " << squares[ i ] << endl;
return 0;
}
/**
* 这里仅是为简要说明动态分配内存的, 一般对于简单的 IntCell 类,
* 无需这样写代码. 但当遇到复杂的类时, 会看到动态分配内存这种技术很有用且很必要.
*/
#include <iostream>
#include "IntCell.h"
using namespace std;
int main( )
{
IntCell *m; // 声明 m 是一个指针变量, 指向 IntCell 类对象. m 在这里未进行初始化
m = new IntCell( 0 );// 假设这里抛出异常, 则根本不会执行到下面的 delete m; 语句
m->write( 5 );
cout << "Cell contents: " << m->read( ) << endl;
delete m;// 不就地处理异常会很危险
return 0;
}
在 C++ 中有两种方式可以使用零参数构造函数来创建对象.
m = new IntCell(); // OK, 但有可能被误认为是函数的声明. m = new IntCell; // OK, 最好采用这种.
在一些语言中, 当一个对象不再引用时, 就纳入自动垃圾收集的范围. 如 Java
C++ 没有垃圾收集. 当一个通过
否则, 该对象所占的内存就不能被释放(直到程序结束). 这被称为
如果指针变量指向
C++ 允许对指针进行各种特殊的运算, 这些运算偶尔是很有用的. 如: 比较运算符
C++ 有三种不同的方式来传递参数.
/** 下面定义的函数 avg 返回数组 arr 中前 n 个整数的平均值. * 并且如果 n 大于 arr.size() 或者小于 1, 就设定 errorFlag 为 true. */ double avg( const vector<int> & arr, int n, bool & errorFlag);
对象的返回也可以是
/** OK
* arr[maxIndex] 索引的 vector 是在 findMax 外部的, 并且存在时间长于调用返回的时间.
*/
const string & findMax( const vector<string> & arr )
{
int maxIndex = 0;
for( int i = 1; i < arr.size( ); i++ )//这里应将 int 改为 unsigned int, 否则编译器警告: 在无符号数与有符号数之间比较
if( arr[ maxIndex ] < arr[ i ] )
maxIndex = i;
return arr[ maxIndex ];
}
/** WRONG
* maxValue 是一个局部变量, 当函数返回时就不复存在了.
* Linux 下编译得到警告:返回了对局部变量的‘maxValue’的引用
*/
const string & findMaxWrong( const vector<string> & arr )
{
string maxValue = arr[ 0 ];
for( int i = 1; i < arr.size( ); i++ )
if( maxValue < arr[ i ] )
maxValue = arr[ i ];
return maxValue;
}
在这些情况下, 变量名就是它所引用的对象名的同义词.
作为局部变量, 它们避免了复制的成本, 因此在对含有类类型集合的数据结构进行排序时非常有用.
在 C++ 中, 伴随类的是已经写好的三个特殊函数, 它们是
当一个对象超出其作用域或执行
通常, 析构函数的唯一任务就是释放使用对象时所占有的所有资源. 这其中包括
有一种特殊的构造函数, 用于构造新的对象, 被初始化为相同类型对象的一个副本, 这就是
以
IntCell B = C; IntCell B( C );而不是
B = C; //Assignment operator, discussed later
当
主要的问题出现在类数据成员是指针的情形.
当一个类含有的数据成员为指针并且深复制很重要时, 一般的做法就是必须实现析构函数、
对于
~IntCell(); //destructor IntCell(const IntCell & rhs); //copy constructor const IntCell & operator=(const IntCell & rhs); //operator=
/**
* Fig 1.13 三大函数的默认值
*/
IntCell::~IntCell( )
{
// Does nothing, since IntCell contains only an int data
// member. If IntCell contained any class objects, their
// destructors would be called.
}
//可以使用 IntCell B(C);
IntCell::IntCell( const IntCell & rhs ) : storedValue( rhs.storedValue )
{
}
//operator= 是我们最感兴趣的.
//可以使用 IntCell B = C;
const IntCell & IntCell::operator=( const IntCell & rhs )
{
if( this != &rhs ) // Standard alias test, (别名测试, 以确保没有复制自身)
storedValue = rhs.storedValue;
return *this; // 返回对当前对象的引用, 于是赋值可以链状进行, 如 a=b=c.
}
最常见的默认值不可用的情况是数据成员是指针类型的, 并且
举例说明, 假设
// Fig 1.14 数据成员是指针, 默认值不适用
class IntCell
{
public:
explicit IntCell( int initialValue = 0 )
{ storedValue = new int( initialValue ); }
int read( ) const
{ return *storedValue; }
void write( int x )
{ *storedValue = x; }
private:
int *storedValue;
};
在下面的 Fig 1.15 中暴露了 Fig 1.14 大量的问题.
// Fig 1.15
int f( )
{
IntCell a( 2 );
IntCell b = a;
IntCell c;
c = b;
a.write( 4 );
cout << a.read( ) << endl << b.read( ) << endl << c.read( ) << endl;
return 0;
}
解决的办法如下图(接口与实现已经分离)
// Fig 1.16
class IntCell
{
public:
explicit IntCell( int initialValue = 0 );
IntCell( const IntCell & rhs );
~IntCell( );
const IntCell & operator=( const IntCell & rhs );
int read( ) const;
void write( int x );
private:
int *storedValue;
};
IntCell::IntCell( int initialValue )
{
storedValue = new int( initialValue );
}
IntCell::IntCell( const IntCell & rhs )
{
storedValue = new int( *rhs.storedValue );// 这里先 rhs.storedValue, 注意 rhs 是对象变量
}
IntCell::~IntCell( )
{
//默认, 析构函数体内一般不需要再添加代码, 但是 IntCell 中有指针成员, 有 new 操作, 因此必须释放.
delete storedValue;
}
const IntCell & IntCell::operator=( const IntCell & rhs )
{
if( this != &rhs )
*storedValue = *rhs.storedValue;
return *this;
}
int IntCell::read( ) const
{
return *storedValue;
}
void IntCell::write( int x )
{
*storedValue = x;
}
C++ 语言提供内置的 C 风格的数组类型. 如
int arr1[10];
在上面的定义中, 在编译的时候数组的大小必须是已知的. 10 不可以用变量来代替. 如果数组的大小未知, 就必须显式声明一个指针, 并且用
int *arr2 = new int[n];
现在
delete[] arr2;
内置的 C 风格的字符串是当作
这些字符串有着数组所包含的所有问题, 包括困难的内存管理问题.
标准的
本节讨论在 C++ 中如何用
函数模板不是真正的函数, 而是一个用以生成函数的模式(pattern).
/**
* Fig 1.17 findMax 函数模板 (与 Fig 1.12 关于 string 的 findMax 版本几乎完全一致)
* Return the maximum item in array a.
* Assumes a.size( ) > 0.
* Comparable objects must provide operator< and operator=
*/
template <typename Comparable>
const Comparable & findMax( const vector<Comparable> & a )
{
int maxIndex = 0;
for( int i = 1; i < a.size( ); i++ )
if( a[ maxIndex ] < a[ i ] )
maxIndex = i;
return a[ maxIndex ];
}
/**
* Fig 1.18 使用 findMax 函数模板
*/
int main( )
{
vector<int> v1( 37 );
vector<double> v2( 40 );
vector<string> v3( 80 );
vector<IntCell> v4( 75 );
// Additional code to fill in the vectors not shown
cout << findMax( v1 ) << endl; // OK: Comparable = int
cout << findMax( v2 ) << endl; // OK: Comparable = double
cout << findMax( v3 ) << endl; // OK: Comparable = string
cout << findMax( v4 ) << endl; // Illegal; operator< undefined
return 0;
}
函数模板可以应需要而自动扩展. 要注意的是随着每种类型的扩展, 都会生成附加代码. 在大项目里, 这被称为
需要注意的是, 上面调用
if( a[ maxIndex ] < a[ i ] ) // 这一行变成非法的
原因在于没有为类
因此, 在使用任何模板之前, 习惯上都是
有很多处理函数模板的晦涩的规则. 大多数的问题都是出现在函数模板不能提供完全匹配的而只是相近的参数(通过隐式类型转换)的情况下. 必须有解决这种不确定问题的办法, 其规则也是非常复杂的.
注意:
完成
这里给出的例子, 类模板与函数模板的运行情况相似.
/**
* Fig 1.19 未分离的 MemoryCell 类模板
* A class for simulating a memory cell.
*/
template <typename Object>
class MemoryCell
{
public:
explicit MemoryCell( const Object & initialValue = Object( ) ) // 注意
: storedValue( initialValue ) { }
const Object & read( ) const
{ return storedValue; }
void write( const Object & x )
{ storedValue = x; }
private:
Object storedValue;
};
要注意的是,
Fig 1.20 给出了
//Fig 1.20
int main( )
{
MemoryCell<int> m1;
MemoryCell<string> m2( "hello" );
m1.write( 37 );
m2.write( m2.read( ) + "world" ); // 这里 + 是指字符串的连接
cout << m1.read( ) << end1 << m2.read( ) << end1;
return 0;
}
注意
实验的结果是, 运行速度比较慢, 需要约 1.6 秒. 这也是使用模板的代价.
目前模板的分离编译在许多平台上都不能很好地运行. 因此, 在许多情况下, 包括其实现的整个类都必须放在
在本书中, 我们总是将
Fig 1.21 给出了一个实现时需要使用
/**
* Fig 1.21, Comparable 可以是类类型, 例如这里定义的 Employee 类类型
* 注意, 如果分离编译, 则声明 string 类时, 最好写为 std::string, 下面的 ostream 也最好改为 std::ostream
* 否则编译会提示
* error: ISO C++ forbids declaration of 'string' with no type
* 如将 Employee 类, findMax 函数模板, main() 函数都放在同一文件中, 则只要在开始时写入 using namespace std; 即可.
*/
class Employee
{
public:
void setValue( const std::string & n, double s )
{ name = n; salary = s; }
const std::string & getName( ) const
{ return name; }
void print( std::ostream & out ) const
{ out << name << " (" << salary << ")"; }
bool operator< ( const Employee & rhs ) const
{ return salary < rhs.salary; }
// Other general accessors and mutators, not shown
private:
std::string name;
double salary;
};
// 操作符重载可以允许重新定义内置操作符的含义
// 为新的类类型 Employee 的输出重新定义操作符<<, 其中用到了上面定义的 print 函数.
// Define an output operator for Employee
std::ostream & operator<< ( std::ostream & out, const Employee & rhs )
{
rhs.print( out );
return out;
}
int main( )
{
vector<Employee> v( 3 );
v[0].setValue( "George Bush", 400000.00 );
v[1].setValue( "Bill Gates", 2000000000.00 );
v[2].setValue( "Dr. Phil", 13000000.00 );
cout << findMax( v ) << endl;
return 0;
}
这样
为具有实用性, 每一个数据成员都必须是
Fig 1.21 例举了
完成 Fig 1.21 的实验.
函数模板可以用来实现泛型算法. Fig 1.17 给出了一个用函数模板查找数组中最大项的例子. 但是模板有一个重要的限制:
在许多情况下, 这并不可行. 例如
这些问题的解决方案就是重写
在实际效果上, 数组对象不再知道如何进行相互比较, 取而代之的是, 这些信息完全从数组对象中剥离出来.
对象同时包含数据和成员函数, 一个如传递参数一样传递函数的巧妙办法是:
从效果上看, 通过将函数放在对象中实现了函数的传递. 该对象通常称为
/** Fig 1.22
* 函数对象思想的最简单实现
*/
#include <iostream>
#include <vector>
#include <string>
#include <cstring>
using namespace std;
// Generic findMax, with a function object, Version #1.
// Precondition: arr.size( ) > 0.
template <typename Object, typename Comparator>
const Object & findMax( const vector<Object> & arr, Comparator cmp )
{
unsigned int maxIndex = 0;
for( unsigned int i = 1; i < arr.size( ); i++ )
if( cmp.isLessThan( arr[ maxIndex ], arr[ i ] ) )
maxIndex = i;
return arr[ maxIndex ];
}
class CaseInsensitiveCompare
{
public:
bool isLessThan( const string & lhs, const string & rhs ) const
{ return stricmp( lhs.c_str( ), rhs.c_str( ) ) < 0; }
};
int main( )
{
vector<string> arr( 3 );
arr[ 0 ] = "ZEBRA"; arr[ 1 ] = "alligator"; arr[ 2 ] = "crocodile";
cout << findMax( arr, CaseInsensitiveCompare( ) ) << endl;
return 0;
}
C++ 的
下面的 Fig 1.23 的例程中没有用到函数对象. 其实现是使用标准库函数对象模板
// Fig 1.23, 没有使用函数对象的 findMax 版本
#include <iostream>
#include <vector>
#include <string>
#include <cstring>
using namespace std;
// Generic findMax, with a function object, C++ style.
// Precondition: a.size( ) > 0.
template <typename Object, typename Comparator>
const Object & findMax( const vector<Object> & arr, Comparator isLessThan )
{
unsigned int maxIndex = 0;
for( unsigned int i = 1; i < arr.size( ); i++ )
if( isLessThan( arr[ maxIndex ], arr[ i ] ) )
maxIndex = i;
return arr[ maxIndex ];
}
// Generic findMax, using default ordering.
#include <functional>
template <typename Object>
const Object & findMax( const vector<Object> & arr )
{
return findMax( arr, less<Object>( ) );
}
class CaseInsensitiveCompare
{
public:
bool operator( )( const string & lhs, const string & rhs ) const
{ return stricmp( lhs.c_str( ), rhs.c_str( ) ) < 0; }
};
int main( )
{
vector<string> arr( 3 );
arr[ 0 ] = "ZEBRA"; arr[ 1 ] = "alligator"; arr[ 2 ] = "crocodile";
cout << findMax( arr, CaseInsensitiveCompare( ) ) << endl;//本质上调用了 stricmp(), 返回 ZEBRA
cout << findMax( arr ) << endl;//调用了 std::less::operator(), 返回 crocodile
return 0;
}
C++ 通常不提供标准的
我们自己也可以很快写出一个合理的
这样做需要附加的
下面给出了一个完整的
#ifndef MATRIX_H
#define MATRIX_H
#include <vector>
using namespace std;
template <typename Object>
class matrix
{
public:
matrix( int rows, int cols ) : array( rows )
{
for( int i = 0; i < rows; i++ )
array[ i ].resize( cols );
}
//后面会详细解释为什么需要这样两个版本(访问版本和修改版本)的 operator[]
const vector<Object> & operator[]( int row ) const
{ return array[ row ]; }
vector<Object> & operator[]( int row )
{ return array[ row ]; }
int numrows( ) const
{ return array.size( ); }
int numcols( ) const
{ return numrows( ) ? array[ 0 ].size( ) : 0; }
private:
vector< vector<Object> > array;
};
#endif
如果有一个
现在我们已经明白
考虑下面的方法(忽略混淆和大小不兼容的可能性, 这两者都不影响算法).
void copy(const matrix<Object> & from, matrix<Object> & to)
{
for(int i=0;i < to.numrows();i++)
to[i]=from[i];
}
注意, 这里参数
假设只定义一个
因为
参考 https://www.cnblogs.com/ArsenalfanInECNU/p/18080526
计算正整数 $n$ 的二进制展开中数字 $1$ 的个数.
int countOnes(unsigned int n)
{
int ones = 0;
while (0 < n)
{
ones++;
n&=n-1;
}
return ones;
}
网站 www.atzjg.net 主要是关于数学的, 同时也有一些计算机方面的资料. 在首页上可以点击 Register 注册(找到自己的班级, 以学生身份注册).