导言
大一下学期的数据结构课使用的语言是C#,这是一门以前我从来没有接触过的语言。不过凭着自己还有点C++的底子,硬是花了两个晚上把课上可能要用到的内容啃完了。至于.NET相关的部分,暂时用不到,也就暂时没有去碰了。(压力是唯一的动力,对于我这种懒惰的人而言233333)
个人对C#的评价,就是一门介于C++和JAVA之间的语言。纯面向对象、所有函数都包含在类当中的架构,确确实实地体现了C#为.NET而生的本质:一门做UI的语言。C#个人感觉把面向对象的模式发挥到了极致,当然也带来了一些依赖性问题,这些暂且就不在我们的讨论范围内了。我们只需要知道,对面向对象的强烈支持,确实能够让我们更加容易地做出各种容器,我想这大概也是老师选用C#来上数据结构课的原因。
那么首先最简单的数据结构就是变长数组,也就是顺序表。我们的目的是要制作一个数据结构,内部维护一个数组,可以在数组内增删改找数据,并且在数据增减时自动调整数组的大小。
总结一下,一个该顺序表实例拥有以下功能:
- 使用数组或其他该顺序表实例初始化
- 以下标方式访问元素
- 获取目前拥有的元素个数
- 在任意位置加入任意个数元素
- 覆盖任意位置的任意个数元素
- 删除任意位置的任意个数元素
- 从左到右或从右到左查找元素并返回下标
- 排序
1.1核心成员
在C#类库项目中,首先新建一个名叫DSAGL的namespace,在其中包含一个泛型类SequencedList(C#中的泛型就是T,简单来说就是可以把这个T在类实例化的时候替换成任何类型)。这个泛型类中的泛型继承自接口IComparable,这就要求这里面储存的泛型必须是可比较的,使得内建排序算法成为可能。
namespace DSAGL
{
public class SequencedList<T> where T : IComparable
{
}
}
类中含有三个成员,一个是我们顺序表的核心数组content,一个是顺序表的长度length,即目前储存的元素个数,最后一个max则是目前顺序表最多能储存的元素个数,它是128的整数倍。当元素个数超过max时,max会被增加128的整数倍,核心数组content的长度也会被增加同样的数值。同样地,当元素个数减少到“max-(128的整数倍)”以下时,max与核心数组content的长度也会被相应减少。
三个成员都被声明为私有,以符合OOP的精神~
private T[] content;
private int length;
private int max;
1.2基本属性
C#的类所拥有的非常强大的功能就是属性和索引器。前者相当于C++中一些进行返回或设置成员变量的值的简单操作的内联函数,后者相当于对[]的重载。我们提供只读的getLength属性以便外界读取length成员,同时保持对length的封装。另外提供索引器来使得外界可以通过访问普通数组的方式来访问容器内的数组。
public int getLength//获取长度
{
get { return length; }
}
public T this[int i]//顺序表的索引器
{
get
{
if (i < 0 || i >= length)
return default(T);
return content[i];
}
set
{
if (i >= 0 && i < length)
content[i] = value;
}
}
可以看到索引器对于一些非法的访问进行了屏蔽,以提高容器安全性。
1.3构造函数
SequencedList提供了三种构造函数,分别是默认的空初始化、数组初始化和利用SequencedList的初始化。
public SequencedList()//默认初始化
{
content = new T[128];
length = 0;
max = 128;
}
public SequencedList(SequencedList<T> tar)//使用SequencedList初始化
{
content = new T[(tar.getLength / 128 + 1) * 128];
for (int i = 0; i < tar.getLength; i++)
content[i] = tar[i];
length = tar.getLength;
max = (tar.getLength / 128 + 1) * 128;
}
public SequencedList(T[] tar)//使用数组或数据列表初始化
{
content = new T[(tar.Length / 128 + 1) * 128];
for (int i = 0; i < tar.Length; i++)
content[i] = tar[i];
length = tar.Length;
max = (tar.Length / 128 + 1) * 128;
}
对于默认初始化,只需要为数组分配默认最小的128个元素的空间,并将max调成默认最小的128,将length更改为容器空时的0即可。而在使用数组和SequencedList初始化的时候,则要根据目标大小调整content的尺寸、max的数值。容器的length将等于目标的中含有的元素个数。
1.4 resize函数
这个函数的作用很明了,也很重要,它是实现变长数组的核心。通过新建一个128rank大小的数组并把原数组的数据复制到新数组当中来实现数组大小的变化*,而在没有delete关键词的C#中,原数组会在一段时间内自动被C#的垃圾回收器回收。
private void resize(int rank)
{
T[] newContent = new T[128 * rank];
Array.Copy(content, newContent, length);
content = newContent;
max = 128 * rank;
}
1.5添加元素函数
插入元素一共提供了支持单个元素插入、从SequencedList插入和从数组插入三个版本,每个版本又有重载,共计8个函数。。。
public void add(T tar)//在容器尾加入元素
{
int pos = length;
if (pos >= max - 1)
resize(max / 128 + 1);
length += 1;
for (int i = length; i > pos; i--)
content[i] = content[i - 1];
content[pos] = tar;
}
public void add(T tar, int pos)//在pos前插入元素
{
if (pos < 0 && pos >= length)
return;
length += 1;
for (int i = length; i > pos; i--)
content[i] = content[i - 1];
content[pos] = tar;
}
public void add(T[] tar)//从数组复制并在容器尾加入元素
{
int startOfMe = length,num=tar.Length,startOfTar=0;
if (length + num >= max - 1)
resize((length + num) / 128 + 1);
length += num;
for (int k = startOfMe; k < length - num; k++)
content[k + num] = content[k];
int i = 0, j = 0;
while (j < num)
content[startOfMe + i++] = tar[startOfTar + j++];
}
public void add(T[] tar,int startOfMe, int startOfTar = 0)//从数组startOfTar位置开始复制并在startOfMe之前插入元素
{
int num = tar.Length;
if (startOfMe < 0 && startOfMe >= length)
return;
if (startOfTar < 0 || startOfTar >= tar.Length)
return;
if (length + num >= max - 1)
resize((length + num) / 128 + 1);
length += num;
for (int k = startOfMe; k < length - num; k++)
content[k + num] = content[k];
int i = 0, j = 0;
while (j < num)
content[startOfMe + i++] = tar[startOfTar + j++];
}
public void add(T[] tar, int startOfMe,int startOfTar,int num)//从数组startOfTar位置开始复制num个元素并在startOfMe之前插入元素
{
if (startOfMe < 0 && startOfMe >= length)
return;
if (startOfTar < 0 || startOfTar >= tar.Length)
return;
if (num > tar.Length - startOfTar)
num = tar.Length - startOfTar;
if (length + num >= max - 1)
resize((length + num) / 128 + 1);
length += num;
for (int k = startOfMe; k < length - num; k++)
content[k + num] = content[k];
int i = 0, j = 0;
while (j < num)
content[startOfMe + i++] = tar[startOfTar + j++];
}
public void add(SequencedList<T> tar)//从SequencedList复制并在容器尾加入元素
{
int startOfMe = length, num = tar.getLength, startOfTar = 0;
if (length + num >= max - 1)
resize((length + num) / 128 + 1);
length += num;
for (int k = startOfMe; k < length - num; k++)
content[k + num] = content[k];
int i = 0, j = 0;
while (j < num)
content[startOfMe + i++] = tar[startOfTar + j++];
}
public void add(SequencedList<T> tar, int startOfMe, int startOfTar = 0)//从SequencedList的startOfTar位置开始复制并在startOfMe之前插入元素
{
int num = tar.getLength;
if (startOfMe < 0 && startOfMe >= length)
return;
if (length + num >= max - 1)
resize((length + num) / 128 + 1);
length += num;
for (int k = startOfMe; k < length - num; k++)
content[k + num] = content[k];
int i = 0, j = 0;
while (j < num)
content[startOfMe + i++] = tar[startOfTar + j++];
}
public void add(SequencedList<T> tar, int startOfMe, int startOfTar, int num)//从SequencedList的startOfTar位置开始复制num个元素并在startOfMe之前插入元素
{
if (startOfMe < 0 && startOfMe >= length)
return;
if (startOfTar < 0 || startOfTar >= tar.getLength)
return;
if (num > tar.getLength - startOfTar)
num = tar.getLength - startOfTar;
if (length + num >= max - 1)
resize((length + num) / 128 + 1);
length += num;
for (int k = startOfMe; k < length - num; k++)
content[k + num] = content[k];
int i = 0, j = 0;
while (j < num)
content[startOfMe + i++] = tar[startOfTar + j++];
}
插入num个元素的方法就是将startOfTar位置及以后的num个元素向后移动num位,再覆盖startOfTar到startOfTar+num中间的所有元素。计算操作次数的数学期望:平均每次要移动个元素,通过等差数列求和可知每个元素要移动次数的期望是
,因此数学期望就是
,顺序表插入元素的时间复杂度是o(n)
1.6覆盖元素函数
覆盖元素的方法比插入元素更加简单,省去了原有元素的位移操作,需要注意的只是length、max值以及content大小的改变而已
public void cover(T[] tar)//从数组复制并覆盖元素
{
int startOfMe = length, num = tar.Length,startOfTar=0;
int j = 0, i = 0;
while (j < num)
{
if (startOfMe + j >= max - 1)
resize((startOfMe + j) / 128 + 1);
if (startOfMe + j >= length)
length = startOfMe + j + 1;
content[startOfMe + i++] = tar[startOfTar + j++];
}
}
public void cover(T[] tar, int startOfMe, int startOfTar = 0)//从数组startOfTar位置开始复制并从startOfMe开始覆盖元素
{
int num = tar.Length;
if (startOfMe < 0 && startOfMe >= length)
return;
if (startOfTar < 0 || startOfTar >= tar.Length)
return;
int j = 0, i = 0;
while (j < num)
{
if (startOfMe + j >= max - 1)
resize((startOfMe + j) / 128 + 1);
if (startOfMe + j >= length)
length = startOfMe + j + 1;
content[startOfMe + i++] = tar[startOfTar + j++];
}
}
public void cover(T[] tar, int startOfMe, int startOfTar, int num)////从数组startOfTar位置开始复制并从startOfMe开始覆盖num个元素
{
if (startOfMe < 0 && startOfMe >= length)
return;
if (startOfTar < 0 || startOfTar >= tar.Length)
return;
if (num > tar.Length - startOfTar)
num = tar.Length - startOfTar;
int j = 0, i = 0;
while (j < num)
{
if (startOfMe + j >= max - 1)
resize((startOfMe + j) / 128 + 1);
if (startOfMe + j >= length)
length = startOfMe + j + 1;
content[startOfMe + i++] = tar[startOfTar + j++];
}
}
public void cover(SequencedList<T> tar)//从SequencedList复制并覆盖元素
{
int startOfMe = length, num = tar.getLength, startOfTar = 0;
int j = 0, i = 0;
while (j < num)
{
if (startOfMe + j >= max - 1)
resize((startOfMe + j) / 128 + 1);
if (startOfMe + j >= length)
length = startOfMe + j + 1;
content[startOfMe + i++] = tar[startOfTar + j++];
}
}
public void cover(SequencedList<T> tar, int startOfMe, int startOfTar = 0)////从SequencedList的startOfTar位置开始复制并从startOfMe开始覆盖元素
{
int num = tar.getLength;
if (startOfMe < 0 && startOfMe >= length)
return;
if (startOfTar < 0 || startOfTar >= tar.getLength)
return;
int j = 0, i = 0;
while (j < num)
{
if (startOfMe + j >= max - 1)
resize((startOfMe + j) / 128 + 1);
if (startOfMe + j >= length)
length = startOfMe + j + 1;
content[startOfMe + i++] = tar[startOfTar + j++];
}
}
public void cover(SequencedList<T> tar, int startOfMe, int startOfTar, int num)//从SequencedList的startOfTar位置开始复制并从startOfMe开始覆盖num个元素
{
if (startOfMe < 0 && startOfMe >= length)
return;
if (startOfTar < 0 || startOfTar >= tar.getLength)
return;
if (num > tar.getLength - startOfTar)
num = tar.getLength - startOfTar;
int j = 0, i = 0;
while (j < num)
{
if (startOfMe + j >= max - 1)
resize((startOfMe + j) / 128 + 1);
if (startOfMe + j >= length)
length = startOfMe + j + 1;
content[startOfMe + i++] = tar[startOfTar + j++];
}
}
1.7删除元素函数
删除元素函数支持删除从start开始的所有元素和从start开始的num个元素两个版本。
public void delete(int start = 0)//删除元素
{
if (start < 0 || start >= length)
return;
int num = length-start;
for (int i = start; i + num < length; i++)
content[i] = content[i + num];
length -= num;
if (length < 0)
length = 0;
if(length<max-128)
resize(length / 128 + 1);
}
public void delete(int start,int num)//删除元素
{
if (num <= 0)
return;
if (start + num > length)
num = length - start;
if (start < 0 || start >= length)
return;
for (int i = start; i + num < length; i++)
content[i] = content[i + num];
length -= num;
if (length < 0)
length = 0;
if (length < max - 128)
resize(length / 128 + 1);
}
删除元素的做法是把从start开始的num个元素赋值为num个之后的元素,并且重设length,这步操作的时间复杂度依然是o(n)的。为了节省空间,元素减少的时候会调用resize函数将content的大小改变,相应地max的值也会修改。
1.8查找函数
查找函数分为从左向右查找和从右向左查找。对于顺序表而言,查找只能使用o(n)的顺序查找,也就是通过迭代下标的方式,寻找值与目标相同的元素,然后将下标返回。
public int find(T tar, int start = 0)//从start开始从左到右查找元素
{
if (start < 0 || start >= length)
return -1;
int end = length-1;
int id = start;
while(id<=end)
{
if (content[id].CompareTo(tar)==0)
return id;
id++;
}
return -1;
}
public int find(T tar, int start,int end)//从左到右查找start与end之间的元素
{
if (start < 0 || start >= length||end<start||end<0||end>=length)
return -1;
int id = start;
while (id <= end)
{
if (content[id].CompareTo(tar) == 0)
return id;
id++;
}
return -1;
}
public int reverseFind(T tar, int start = 0)//从start开始从右到左查找元素
{
if (start < 0 || start >= length)
return -1;
int end= length-1;
int id = end;
while(id>=start)
{
if (content[id].CompareTo(tar)==0)
return id;
id--;
}
return -1;
}
public int reverseFind(T tar, int start, int end)//从右到左查找从start到end的元素
{
if (start < 0 || start >= length || end < start || end < 0 || end >= length)
return -1;
if (end < 0)
end = length - 1;
int id = end;
while (id >= start)
{
if (content[id].CompareTo(tar) == 0)
return id;
id--;
}
return -1;
}
1.9排序函数
顺序表由于可以直接通过地址访问元素,寻址是o(1)的,因此直接利用下标对content进行快速排序即可。
private void quickSort(int s, int e)
{
if (s < e)
{
int i, j;
T x1, x2;
i = s;
j = e;
x1 = content[i];
x2 = content[i];
while (i < j)
{
while (i < j && content[j].CompareTo(x1)>0)
j--;
if (i < j)
content[i++] = content[j];
while (i < j && content[i].CompareTo(x1)<0)
i++;
if (i < j)
content[j--] = content[i];
}
content[i] = x2;
quickSort(s, i - 1);
quickSort(i + 1, e);
}
}
private void reQuickSort(int s, int e)
{
if (s < e)
{
int i, j;
T x1, x2;
i = s;
j = e;
x1 = content[i];
x2 = content[i];
while (i < j)
{
while (i < j && content[j].CompareTo(x1) < 0)
j--;
if (i < j)
content[i++] = content[j];
while (i < j && content[i].CompareTo(x1) > 0)
i++;
if (i < j)
content[j--] = content[i];
}
content[i] = x2;
reQuickSort(s, i - 1);
reQuickSort(i + 1, e);
}
}
public void sort()//从大到小快速排序
{
quickSort(0, length-1);
}
public void reSort()//从小到大快速排序
{
reQuickSort(0, length - 1);
}
关于栈和队列
顺序表的特性让其可以非常容易地实现栈和队列有关操作。所谓栈就是一个first in last leave的数据结构,最早通过push操作压栈的元素,最晚通过pop操作离栈。而队列则是first in first leave,最早通过enqueue操作入队的元素被作为队头,之后又最早通过lvqueue操作离开。说白了就是一个通过特殊顺序增减元素的顺序表。
2.1栈和队列有关函数
栈的压栈和队列的进队都可以调用缺省的add函数实现。栈的出栈则是返回顺序表最后一个元素的值,并将length的值减1。
public T pop()//栈顶出栈
{
if (length == 0)
return default(T);
T obj = content[length - 1];
delete(length - 1, 1);
return obj;
}
队列出队假如通过返回第一个元素的值并删除第一个元素的方式实现,由于顺序表删除元素的操作次数期望为,而删除队首元素占到了最坏情况下的
次,这无疑是低效的。因此队列操作不能通过我们上文所提到的函数进行。
因此为了实现队列操作,我们还要维护一个成员变量head用于保存队头的下标。出队便是返回下标head所示的元素,并将head的值加1。当head的值等于length的值时,表示队空。
private int head;//记得在构造函数中初始化head的值为0
public int Head//获取与设置队头
{
get { return head; }
set { head=value; }
}
public T start()//队头出队
{
if (head == length)
return default(T);
T obj = content[head];
head++;
return obj;
}
2.2利用队列进行进制转换
下面的源码将使用SequencedList类的队列功能将任意进制数转为十进制数。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DSAGL;
namespace AnyToDEC
{
class Program
{
static void Main(string[] args)
{
int basis, target ;
while (true)
{
SequencedList<int> list = new SequencedList<int>();
int tmp = 0, round = 0;
Console.Write("输入现有数进制:");
basis = int.Parse(Console.ReadLine());
Console.Write("输入现有数:");
target = int.Parse(Console.ReadLine());
while (target>0)
{
list.add(target % 10);
target /= 10;
}
while (list.Head != list.getLength)
{
tmp += list.start() * (int)Math.Pow(basis, round);
round++;
}
Console.Write("十进制模式:{0}\n",tmp);
}
}
}
}
大体算法是将原数对10取余后入队,原数除以10得到新数,再用新数代替原数。令队列中每个数的数值为,队列总共有
位,原数进制为
,那么计算公式便是:
让队列中每个元素出队进行运算即可。
2.3利用栈进行四则运算
栈的特性可以将中缀表达式转换为后缀表达式,从而实现机器的代数式四则运算。
举个例子,这个符合我们人类算术习惯表达式就是中缀表达式,而这个中缀表达式的后缀表达式则是
,它的运算方法是从左到右不区分运算符优先级,遇到数字则压栈,遇到符号则让两个数出栈,进行对应运算后再压栈,这是符合机器计算方式的表达式。中缀表达式转后缀表达式的关键就是在考虑运算符优先级的前提下扫描中缀表达式,对数字栈和运算符栈进行操作。
中缀表达式转后缀表达式+后缀表达式运算操作的要点如下:
- 数字时,加入数字栈
- 运算符:
1. 若为 '(',加入符号栈
2. 若为 ')',符号栈中符号依次出栈,,直到出现'(',从符号栈中删除'('
3. 若为除括号外的其他运算符,当其优先级高于除'('以外的栈顶运算符时,直接入栈。否则从栈顶开始,依次弹出比当前处理的运算符优先级高和优先级相等的运算符,直到一个比它优先级低的或者遇到了一个左括号为止,然后将其自身压入栈中
4. 中缀表达式扫描结束时,所有运算符出栈 - 每次符号栈中的符号出栈,让数字栈中的两个数出栈进行运算后再加入数字栈
我们用以上方法计算例子中的中缀表达式。
- 4入数字栈
- +入符号栈
- 2入数字栈
- *入符号栈
- 3入数字栈
- *出符号栈,3和2出数字栈,2*3=6,6入数字栈
- +出符号栈,6和4出数字栈,4+6=10,10入数字栈
- -入符号栈
- 10入数字栈
- \入符号栈
- 5入数字栈
- \出符号栈,5和10出数字栈,10\5=2,2入数字栈
- -出符号栈,2和10出数字栈,10-2=8,最终结果为8
以上程序源码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DSAGL;
namespace calc
{
class Program
{
static bool Compare(char a, char b)
{
int rankA=0, rankB=0;
if (a == '(' || b == '(')
return false;
if (a == '+' || a == '-')
rankA = 1;
if (a == '*' || a == '/')
rankA = 2;
if (b == '+' || b == '-')
rankB = 1;
if (b == '*' || b == '/')
rankB = 2;
if (rankA > rankB)
return false;
else
return true;
}
static double calc(double a,double b,char op)
{
if (op == '+')
return a + b;
if (op == '-')
return a - b;
if (op == '*')
return a * b;
if (op == '/')
return a / b;
return 0;
}
static void Main(string[] args)
{
string target;
double value1, value2;
while (true)
{
SequencedList<double> values = new SequencedList<double>();
SequencedList<char> operators = new SequencedList<char>();
int start = 0, end = 0;
target = Console.ReadLine();
while(true)
{
if (start == target.Length)
break;
while((target[start]< 48 || target[start] > 57)&&target[start] != 46)
{
if (operators.getLength == 0)
{
operators.add(target[start]);
start++;
end++;
}
else if (target[start] == ')')
{
while (operators[operators.getLength - 1] != '(')
{
value2 = values.pop();
value1 = values.pop();
values.add(calc(value1, value2, operators.pop()));
}
operators.pop();
start++;
end++;
}
else
{
while (Compare(target[start], operators[operators.getLength - 1]))
{
value2 = values.pop();
value1 = values.pop();
values.add(calc(value1, value2, operators.pop()));
if (operators.getLength == 0)
break;
}
operators.add(target[end]);
start++;
end++;
}
}
while ((target[end]>=48&& target[end] <= 57)||target[end]==46)
{
end++;
if (end == target.Length)
break;
}
values.add(double.Parse(target.Substring(start, end - start)));
start = end;
}
while (operators.getLength>0)
{
value2 = values.pop();
value1 = values.pop();
values.add(calc(value1, value2, operators.pop()));
}
Console.Write("{0}\n", values.pop());
}
}
}
}
结语
懒得写了,反正这个还有下一期。非要说什么的话……markdown真是让我打开了新世界的大门啊