前言:入行快一年了第一次写博客。其实寻思了一下也没啥能写的,毕竟学艺不精,不过看了好多本数据结构的书,总结一下,也是很好的,顺便帮助一下比我更菜的人...如有问题请指出,不胜感激。
其实网上关于类似的博客很多,但是好多大神会、懂,但是不一定会讲,因为他们早已忘了菜是什么感觉了。 = =,而我不一样,我会把我学习过程中遇到问题都拿出了,希望对你们有帮助。
一,什么是列表?
1,列表是什么意思?
英文含义(有道翻译)
List = 列表;清单;目录。
文字含义(百度百科)
列表 = 以表格为容器,装载着文字或图表的一种形式,叫列表。
从字面含义上可以知道,列表就是用来装东西的。而在编程语言中,这里的东西就是各种指的各种类型。
列表总体上来说就是一种容器。
当然人分亚洲人欧洲人,列表也分好多种,这里全文上下所指的都是List这个列表,概念不一定可以泛用,这里注意一下。
2,容器的概念
把一个球放在瓶子里,然后瓶子就是这个球的容器,这是理所当然的。
其实编程中的容器,我更趋向于这种例子,有一块黑布,中间有个球大小的洞,你通过洞把球放进去,你也可以通过这个洞把球取出来,但是这个黑布下算不算容器,是什么你却不清楚。但是你所知的是,你可以从这个洞放,和取。
正如我们使用时的Add和Remove
我所理解的是,洞是瓶子的入口,而瓶子在编程中抽象成了一种规则。
3,瓶子的规则
这个地方,或许就很懵逼了,其实我也很懵逼,因为这是我现想的。
但是我可以说一下什么是规则。
还是那个例子,假设黑布下面什么都木有,所以其空间很大,但是你扔进去了很多球,不可能一个不漏的把球再拿出来吧?
为了避免你拿球的时候漏球,黑布下面的“容器”就开始操作了。
第一种方法,一球一槽
“容器”偶然发现了你球的发票,购买物品:球,数量:5
原来你有5个球,啊,然后容器变出来一个装球的槽,大概是这样子的
接下来你每次从洞里放进一个球,它就把它放在这个槽里,这样你想要第几个,或者全部取出来,都是很容易的。
知识点:
知道球的确定数量才可以这样做。
槽是不可扩充的。
取球很方便,直接拿就可以,毕竟没有盖子...
无法添加球,理论不可移除球,除非换个槽
第二种方法,连串串
这一次你买球的时候并没有要发票,"容器就不知道你有多少球了",但是丝毫不慌,它又有新方法了
当你放第一个球的时候,这里假定第一个球为A,“容器”偷偷的虚拟出来另一个球,俗称“头球”!
“容器”对“头球”说,“阿头,去发展下线吧!”
然后头球给A电话,A一看手机“未知号码”?
接通电话,头球:“我是你的上级,我在监视你,在这里只有我知道你的身份,如果你想活下去,就替我监视下一个人,顺便传达这番话”
然后A对B说,B对C说.....
于是就成了这个样子
这样不管来了多少球,它们都是关联的,只需要找到头球,依次找下线就行了...
知识点:
每个球都不知道自己上级是谁。
每个球都有唯一一个下级
必须要有头球。
取球很麻烦,必须从头球开始依次访问,直到找到指定球
添加很方便,移除很方便,比如移除A,头球的下线换成B,就阔以了
4,总结
1,黑布之下的空间=内存
2,一球一槽=顺序存储
3,连串串=链式存储
4,数组=顺序储存;列表=链式储存(单向链表)
5,以上说的是内存存储数据的两种方式,并不代表其它的容器类型一定是这两种,或者不是这两种。
二,C#代码实现系统自带List的部分功能
1,意义
实用意义:自带的容器不一定有你想要的方法,效率不一定有你自己实现的高
其它:几个月前面试的时候,让我实现链表,写不出来,报仇雪恨。只想搬砖的码农不是好码农...
2,自定义节点类
我们知道列表其实是链表后,其实我们只需要实现链表就行了。
节点类
class Node<T>
{
public T data;//储存的数据
public Node<T> Next;//自己关注的对象
public Node(T data)//赋值用的构造方法
{
this.data = data;
}
public Node()//头结点没值,所以专门准备了一个构造方法
{
}
}
3,自定义列表类
class MyList<T>
{
public MyList()//初始化链表的时候创建一个头节点
{
firstNode = new Node<T>();
}
/// <summary>
/// 头结点
/// </summary>
private Node<T> firstNode;
/// <summary>
/// 列表长度
/// </summary>
public int Count { private set; get; }
}
上边也说了,不管你有没有数据,只要是链表表,就要有头节点
以下代码,全在myList内添加,所以只贴片段。
4,实现Add方法
/// <summary>
/// 始终是最后一个
/// </summary>
private Node<T> lastOne;
/// <summary>
/// 添加数据
/// </summary>
/// <param name="t"></param>
public void Add(T t)
{
Node<T> node = new Node<T>(t);
if (Count == 0)
{
firstNode.Next = node;
}
else
{
lastOne.Next = node;
}
lastOne = node;
Count++;
}
新增一个数据,就new一个节点,这个节点包含当前数据和下一个节点信息
这个地方多了一个变量,为了添加方便,我们缓存一下最后一个节点,因为添加都是在最后一个节点里添加的
为了方便,新数据直接放到最后一个节点里,再把新数据当做最后一个节点。
第一个判断就简单了,没数据,就把头结点指向第一个节点
5,自定义Find方法
这个方法是列表类自己用的
/// <summary>
/// 找到指定节点
/// </summary>
/// <param name="index">找到第几个节点</param>
/// <returns></returns>
private Node<T> Find(int index)
{
if (index > Count)
return null;
Node<T> tempNode = firstNode;
int tempIndex = 0;
while (tempNode.Next != null)
{
if (tempIndex != index)
{
tempNode = tempNode.Next;
tempIndex++;
}
else
{
return tempNode.Next;
}
}
return null;
}
最后return 返回想要节点的下一个是为什么?
比如我们想找第0个数据,其实是头结点的下一个,类推,找第N个数,就是N.next才是它的数据
因为多了一个头节点,值得注意的是链表的每次查找时间复杂度都是O(n),横向对比线性表为O(1)
6,索引器
有添加了和查找了,我们就可以存,取了
public T this[int index]
{
get
{
if (index > Count)
throw new System.Exception("超出索引");
return Find(index).data;
}
}
添加一个索引器。
7,移除数据
/// <summary>
/// 移除数据
/// </summary>
/// <param name="index"></param>
public void RemoveAt(int index)
{
if (index>Count)
throw new System.Exception("超出索引");
var valueUp = index == 0 ? firstNode : Find(index - 1);
var value = valueUp.Next;
valueUp.Next = value.Next;
value = null;
Count--;
}
假设移除B,那么我们先找到A,代码中的Find(index-1)
然后A的Next等于B的Next
B就没人关注了,B=null,手动释放一下。
长度减1。
8,foreach遍历
这里我们需要继承一个接口IEnumerable
foreach遍历的时候其实就是执行一下GetEnumerator()这个方法。
我们在方法里把需要的数据返回出来就可以了。
int tempCurrent = -1;
public IEnumerator GetEnumerator()
{
while (tempCurrent<Count-1)
{
tempCurrent++;
yield return Find(tempCurrent).data;
}
tempCurrent = -1;
}
我把所有的数据在这里遍历了一遍,其实这里我感觉自己错了,但是说不上来哪不对...
但是有一个好处是可见的,就是继承这个接口后,可以在初始化的时候赋值了。
比如这样
MyList<string> myList = new MyList<string>()
{
"哟","和","啊"
};
我一开始并不懂这是怎么做到的,直到我把Add这个方法注释之后,你在初始化时赋值就会报错。
原来它把初始化时赋的值都调用了Add这个方法,当参数依次传了进去。
然后功能基本齐全了。
三,完整代码,和使用方式
class MyList<T>:IEnumerable
{
public MyList()
{
firstNode = new Node<T>();
}
/// <summary>
/// 头结点
/// </summary>
private Node<T> firstNode;
/// <summary>
/// 始终是最后一个
/// </summary>
private Node<T> lastOne;
/// <summary>
/// 长度
/// </summary>
public int Count { private set; get; }
public T this[int index]
{
get
{
if (index>Count)
{
throw new System.Exception("超出索引");
}
return Find(index).data;
}
}
/// <summary>
/// 添加数据
/// </summary>
/// <param name="t"></param>
public void Add(T t)
{
Node<T> node = new Node<T>(t);
if (Count == 0)
{
firstNode.Next = node;
}
else
{
lastOne.Next = node;
}
lastOne = node;
Count++;
}
/// <summary>
/// 找到指定节点
/// </summary>
/// <param name="index">找到第几个节点</param>
/// <returns></returns>
private Node<T> Find(int index)
{
if (index > Count)//超出索引,返回
return null;
Node<T> tempNode = firstNode;
int tempIndex = 0;
while (tempNode.Next != null)
{
if (tempIndex != index)
{
tempNode = tempNode.Next;
tempIndex++;
}
else
{
return tempNode.Next;
}
}
return null;
}
/// <summary>
/// 移除数据
/// </summary>
/// <param name="index"></param>
public void RemoveAt(int index)
{
if (index > Count)
throw new System.Exception("超出索引");
var valueUp = index == 0 ? firstNode : Find(index - 1);
var value = valueUp.Next;
valueUp.Next = value.Next;
value = null;
Count--;
}
int tempCurrent = -1;
public IEnumerator GetEnumerator()
{
while (tempCurrent<Count-1)
{
tempCurrent++;
yield return Find(tempCurrent).data;
}
tempCurrent = -1;
}
}
class Node<T>
{
public T data;//储存的数据
public Node<T> Next;//自己关注的对象
public Node(T data)//赋值用的构造方法
{
this.data = data;
}
public Node()//头结点没值,所以专门准备了一个构造方法
{
}
}
测试了一下,和C#List对比
List<string> List = new List<string>()
{
"哟","和","啊"
};
MyList<string> myList = new MyList<string>()
{
"哟","和","啊"
};
List.Add("o");
myList.Add("o");
List.RemoveAt(0);
myList.RemoveAt(0);
print(List.Count);
print(myList.Count);
foreach (var item in List)
{
print(item);
}
foreach (var item in myList)
{
print(item);
}
运行结果一模一样,我就不放出来了
总结
第一次写博客,发现耗时比较长。
过程中又解决了几个之前没注意过的问题,收益立竿见影。
因为没有参照什么文章,或者书籍,实现过程都是自己猜测的,和C#List具体的实现无关,如果有什么不对的地方,请指出,不胜感激。