数据结构第二课-线性表
线性表是具有相同特性的数据元素的一个有限序列。
线性表的顺序存储结构是指用一块地址连续的存储空间依次存储线性表中的数据元素。
特点有:
1、容量固定。
2、访问速度快
性能优点小结:
1、算法实现简单,不会为描述元素之间的逻辑关系而增加额外的内存空间
2、基于索引随机访问的性能较优,一般用于元素增加和删除操作不太多的领域
缺点:
1、数组需要程序员来进行空间预分配,若估计不足,会造成空间不够或浪费。
2、动态数组虽然解决空间固定的问题,但是在频繁添加元素的时候,会导致数组内存空间不断重新分配。
3、数据元素的添加和删除不便,常常导致大规模的数据移动,影响了算法速度。
顺序表之模拟ArrayList
public class MyArrayList
{
private const int _defaultCapacity = 4;
private object[] _item;
private int _size = 0;//当前数组已经装了多少个数据
private static readonly object[] emptyArray = new object[0];
public MyArrayList(){
this._item = emptyArray;
}
public MyArrayList(int size){
if (size<0)
{
throw new ArgumentOutOfRangeException("capacity","为arrayList指定的初始容量不能为负数");
}
this._item = new object[size];
}
#region 从新调整数组的空间大小
public virtual int Capacity{
get { return this._item.Length; }
set{
if (value!=this._item.Length)
{
if(value<this._item.Length){
throw new ArgumentOutOfRangeException("value", "参数太小");
}
if (value > 0)
{
object[] destinationArray = new object[value];
if (this._size == 0)
{
Array.Copy(this._item, 0, destinationArray, 0, this._size);
}
this._item = destinationArray;
}
else
this._item = new object[_defaultCapacity];
}
}
}
private void EnsureCapacity(int minsize){
if(this._item.Length<minsize){
int num = (this._item.Length == 0) ? _defaultCapacity : this._item.Length * 2;
if (num<minsize)
{
num=minsize;
}
this.Capacity = num;
}
}
#endregion
public int Add(object value){
if (this._size==this._item.Length)
{
this.EnsureCapacity(_size+1);
}
return _size++;
}
public virtual void Insert(object value,int index){
if(index<0 || index>this._size){
throw new ArgumentOutOfRangeException("index", "超出索引范围");
}
if(this._item.Length==this._size){
this.EnsureCapacity(_size + 1);
}
if (index<this._size)
{
Array.Copy(this._item, index, this._item, index + 1, this._size - index);
}
this._item[index] = value;
this._size++;
}
public virtual void RemoveAt(int index){
if (index < 0 || index > this._size)
{
throw new ArgumentOutOfRangeException("index", "超出索引范围");
}
this._size--;
if (index < this._size)
{
Array.Copy(this._item, index+1, this._item, index - 1, this._size - index);
}
_item[_size] = null;
}
public virtual void TrimToSize(){
this.Capacity = this._size;
}
public int Count{
get { return this._size; }
}
System.Collections.ArrayList lst = new System.Collections.ArrayList();
}
线性表的链式存储结构--链表
链表和顺序表的区别就是顺序表的元素在存储空间必须处于指定的一个空间内。而链表不需要,
好比顺序表是排队买饭,而链表则是银行办理业务(逻辑关系由那张排号纸决定,不是你坐在银行某个位置)
链表性能小结
优点:
1、链表在需要空间时才申请,不需要则可以释放。
2、链表节点的增删和调整只需要修改几个相关节点的指针地址即可,非常方便。
3、双向链表从两个方向搜索节点,能够大大简化算法的复杂度。
缺点:
1、链表算法实现较为复杂,元素之间的逻辑关系还需要额外的内存空间。
2、链表节点只能顺序访问,随即访问性能不佳。
3、单项链表节点均有其后继节点的地址信息,无法方便、快捷地获取其前驱节点的地址。
4、链表有可能会把它的节点存放在托管堆的任意角落。这使垃圾回收在处理的时候需要花更多力气
单项链表
Node为嵌套类,表示单个节点。每个节点都有个next指针,指明它的下个节点
每个单项链表都带有一个头指针(用head表示),并通过头指针唯一标识该链表。
从头指针所指的头出发可以访问到每个节点。末节点的next指针为空,表示链表结束。
从如下代码中可以看到,增删改查等的操作都需要通过一个getNodeByIndex这样的寻找方法,效率非常低。
在.net类库中,System.Collections.Specialized.ListDictionary类是唯一一个属于单向链表的集合类,它是基于key,value的集合。微软的建议是操作10个以下的节点时使用。
. public class LinkedList{private class Node{public object item;public LinkedList.Node next;public override string ToString(){return item.ToString();}public Node(object value){item = value;}}private int count = 0;private Node head;//头指针public int Count{get{return count;}}private Node getNodeByIndex(int index){if (index < 0 || index > this.count)throw new ArgumentOutOfRangeException("index", "索引值超出");Node tempNode = this.head;for (int i = 0; i < index; i++){tempNode = head.next;}return tempNode;}public void Add(object value){//在链表的尾部加上元素Node newnode = new Node(value);if (head == null)head = newnode;elsegetNodeByIndex(this.count).next = newnode;count++;}public void Insert(int index, object value){if(index < 0 || index > this.count)throw new ArgumentOutOfRangeException("index", "索引值超出");Node newNode=new Node(value);Node tempNode;if(index==0){if(this.head==null){this.head = newNode;}else{tempNode = this.head;this.head = newNode;newNode.next = tempNode;}}else{Node preNode = getNodeByIndex(index-1);tempNode = preNode.next;preNode.next = newNode;newNode.next = tempNode;}}public void RemoveAt(int index){if (index < 0 || index > this.count)throw new ArgumentOutOfRangeException("index", "索引值超出");if(index==0){this.head = this.head.next;}else{Node preNode = getNodeByIndex(index-1);Node target = preNode.next;Node nexNode = target.next;preNode.next = nexNode;}count--;}public object this[int index]{get { return getNodeByIndex(index).item; }set { getNodeByIndex(index).item = value; }}}
循环链表
在单向链表中,每个节点的指针都指向其下一个节点,最后一个节点的指针为空,表示链表的结束。
修改一下末节点的next指针,使它指向第一个节点,那么这个链表就形成了一个环,
这样,在链表中的任一节点出发均可以找到表中的其他的节点。
循环链表的设计:
单向循环节点只需要把Head节点换成Tail(指向表中的最后一个节点)节点,然后通过该节点的next指针就可以访问到第一个节点了。
约瑟夫问题: 15的教徒和15非教徒在海上遇险,必须将一半的人投入海中。于是想了一个办法,30个人围成一个圈,从第一个人开始报数,每数到7(用M表示)就扔入大海,如此循环仅剩15人为止,怎样排这30个人的位置,才能使每次投入大海的都是非教徒?
=======================================================================
这就是一个单循环链表的问题。我们只需要再增加一个currentNode这样的指针,再增加一个
private Node MoveStep(M){……} 这样的方法返回一个节点并删除即可得到每次出列的节点,然后把非教徒排在出列的前15个数字的位置上即可。
可是,这又遇到一个删除节点的性能问题。因为删除了一个节点,必须通过Tail指针循环这个链表才能找到被删除节点的前一个索引。解决这个问题,只需要把currentNode指针换成currentPrev指针(当前指针的前一个,这样只需要把currentPrev这个node的next指针指向被删除节点的下一个指针,就可以修复该链表)
. public class CircularLinkedList{private int count = 0;private Node currentPrev;//使用前驱节点来标志当前节点private Node tail;//末节点private class Node{public Node next;public object item;public Node(object value){item = value;}}public void Add(object value){Node newNode = new Node(value);if (this.count==0){//如果链表为空,那么前驱节点就是本身, 末节点也是本身,currentPrev = newNode;tail = newNode;newNode.next = newNode;}else{newNode.next = tail.next;tail.next = newNode;if(currentPrev==tail){currentPrev = newNode;}tail = newNode;}count++;}public void RemoveCurrentNode(){if (count==0){throw new NullReferenceException("集合中没有任何元素");}else if (count == 1){tail = null;currentPrev = null;}else {if (currentPrev.next == tail)tail = currentPrev;currentPrev.next = currentPrev.next.next;}count--;}public void Move(int step){if(step<0){throw new ArgumentOutOfRangeException("step", "参数不能小于0");}if(this.count==0){throw new NullReferenceException("集合中没有任何值");}for (int i = 1; i <= step; i++){currentPrev = currentPrev.next;}}public int Count{get { return this.count; }}public object Current{get { return currentPrev.next.item; }}}
带密码的约瑟夫问题:
编号为1,2,……n的n个人按照顺时针飞向围坐一圈,每个人有且只有一个密码(正整数)。一开始任选一个正整数作为报数上限值(m),从第一个人开始自1开始报数,报到m时停止并出列。然后他的密码作为新的m值,从他在顺时针方向的下一个人开始从新报数,如此下去,直到所有人出列。
. public class People{public int name{get;set;}public int pwd{get;set;}}class Program{static void Main(string[] args){CircularLinkedwithPwdList cil = new CircularLinkedwithPwdList();Console.WriteLine("初始化……");//实例化10个人, 每个人的密码随即for (int i = 1; i <= 10; i++){Random r = new Random();People p = new People();p.name = i;p.pwd = r.Next(1, 10);cil.Add(p);System.Threading.Thread.Sleep(200);Console.Write("{0}-{1}...",i.ToString(),p.pwd.ToString());if (i == 10)Console.WriteLine();}Console.WriteLine("游戏开始");run(5, cil);Console.ReadKey();}private static void run(int m, CircularLinkedwithPwdList cil){if(cil.Count > 1){People p = null;cil.Move(m);//移动m步,即报数为m的人出列.p = ((People)cil.Current);cil.RemoveCurrentNode();Console.WriteLine(string.Format("当前出列的人是{0},密码是{1}", p.name.ToString(), p.age.ToString()));run(p.pwd, cil);}}}
双向链表
在单链表中,通过一个节点找它的后继节点很容易,但是要找到它的前驱节点则很麻烦,只能通过头节点通过next指针一个个查找。
如果希望方便地查找前驱节点,可以给每个节点加一个指向前驱的指针,使得链表可以双方向查找,这种链表称为双向链表。
泛型集合System.Collections.Generic.LinkedList<T>实现了双向链表,它是一个通用链表类,不支持随即访问(索引访问)

浙公网安备 33010602011771号