C#数据结构(2) 链表

导言

GitHub项目地址:https://github.com/HarmoniaLeo/CSharp-DS

线性表家族除了我们上次分享的顺序表外,另一位重要成员就是链表。链表是一串结点,每个结点维护着自己的数值,以及指向其他结点的指针。指针将结点像锁链的一环一环一样扣在一起,在从一个结点访问另一个结点时,就通过指针来“穿梭”。

链表根据拥有指针的不同,分为只有指向下一个结点指针的单向链表,和同时拥有指向下一个与上一个结点的指针的双向链表。如果最后一个结点的指针指向了第一个结点,还可以实现环状链表。

顺序表的插入与删除涉及到大量元素的位移,其运算次数是和数据规模有关的。但是对于链表来说,只需要改动部分结点指针的指向,把新增的环“扣进”或者“移出”原来的链条就行。然而链表也有自己的缺点,这些都将会在本文中一一讨论。

C#的类对象全部采用引用变量,也就是说所有值面上的“类实例”其实都只是指向类的引用而已,相当于C++的指针。对于初学者以及没有接触过JAVA和Python的人来说可能会有点摸不着头脑,但个人认为这也是C#作为纯面向对象语言在设计模式上做出的一个大胆而合理的简化——毕竟就算是在C++当中,类按指针实例化、按引用传递也是我们的共识。

本次将制作一个名为DoubleLinkedList的容器,这是一个双向环状链表,且具有以下功能:

  • 使用数组或其他该链表实例初始化
  • 使用迭代器访问元素
  • 使用迭代器加入任意个数元素
  • 使用迭代器删除任意个数元素
  • 取子链
  • 连接两条链
  • 从左到右或从右到左查找元素并返回迭代器
  • 排序

1.1迭代器的概念

我们知道数组其实就是连成一片的地址,数组的寻址使用的是指针的方式,在访问指针所指的元素的下一个元素时,只需要将指针所指的地址+1就行。但是,链表的数据组织方式是散列的,这意味着要访问链表中的下一个元素,需要先获取指针目前所指元素中的next指针,也就是:指针=指针->next。

但假如我们把指向链表的指针封装为一个类,为指针=指针->next这个操作重载++运算符,就可以让链表的指针像数组的指针一样方便操作了。再比如,我们再为这个类重载+号,让类对象+6这个操作返回其内部指针循环了6次指针=指针->next操作后形成的新类对象,就可以将原本需要循环六次才能完成的操作缩减为一行代码。这个类就是我们所说的迭代器。

本文中所实现的链表,其内部结构是封闭的。外界对链表的一切初始化、增删改找等操作,都通过迭代器来实现。在DSAGL命名空间里定义iterator泛型类。

namespace DSAGL
{
    public class iterator<T> where T : IComparable
    {
        private DoubleLinkedNode pos;
    }
}

迭代器的成员就是一个DoubleLinkedNode类对象,由于C#中对象是引用,它实际上起到了指针的作用

1.2链表结构的概念

链表中的结点,是一个由数据、指向前一个结点的指针、指向后一个结点的指针组成的结构。为了使得我们的链表更加符合oop的精神,我们将数据设为只读,同时增加一个bool型的avaluable属性,用来指示这个结构是否可用。假如一个结构中的数据未被初始化或者已经被从链表中删除,则avaluable属性为false,否则为true。

结点支持默认初始化、仅数据初始化、利用其他结点中的数据的初始化、数据和前后指针初始化和自身在被删除时的析构。它被声明为迭代器的嵌套类。

public class DoubleLinkedNode
{
    private T data;
    public bool avaluable;
    private DoubleLinkedNode next;
    private DoubleLinkedNode last;

    public DoubleLinkedNode Next { get { return next; } set { next = value; } }
    public DoubleLinkedNode Last { get { return last; } set { last = value; } }
    public T Data { get { return data; } }

    public DoubleLinkedNode()
    {
        next = this;
        last = this;
        avaluable = false;
    }

    public DoubleLinkedNode(T value)
    {
        data = value;
        next = this;
        last = this;
        avaluable = true;
    }

    public DoubleLinkedNode(DoubleLinkedNode value)
    {
        data = value.data;
        next = this;
        last = this;
        avaluable = true;
    }


    public DoubleLinkedNode(T value, DoubleLinkedNode m_last, DoubleLinkedNode m_next)
    {
        if (!m_last.avaluable || !m_next.avaluable)
        {
            data = value;
            next = this;
            last = this;
            avaluable = true;
        }
        data = value;
        next = m_next;
        m_next.last = this;
        last = m_last;
        m_last.next = this;
        avaluable = true;
    }

    public void delete()
    {
        next.last = last;
        last.next = next;
        next = this;
        last = this;
        avaluable = false;
    }
}

1.3迭代器的寻址

迭代器的寻址方式也是迭代器名称的由来:迭代器的每次向后寻址都是将自身所指向的结点设为该结点包含的下一结点引用。由于地址上的不连续性,链表不能像顺序表一样通过常数级别的地址加减来找到别的元素,而是要用时间复杂度为 o ( n ) o(n) 的迭代寻址法,这也是链表和顺序表相比最显著的缺点。

迭代器内封装了私有的findPotion方法。通过传入DoubleLinkedNode对象和寻址的次数,返回一个新的对象,指向参数寻址后指向的DoubleLinkedNode。

private static DoubleLinkedNode findPotion(DoubleLinkedNode start,int id)
{
    if (id >= 0)
    {
        for (int i = 0; i < id; i++)
            start = start.Next;
    }
    else
    {
        for (int i = 0; i > id; i--)
            start = start.Last;
    }
    return start;
}

注意,假如我们把pos对象传给了start参数,我们更改start对象的成员的值,是会更改函数外面pos对象的值的,因为我们通过引用更改了对象本身。但是把start赋值给start.Next这个操作不会更改pos引用指向的对象,因为引用本身是按值传递的。这更加说明了C#的引用传递其实就是缺省的C++中的指针传递。

1.4迭代器的初始化

迭代器的初始化支持利用数值的初始化、利用迭代器的初始化和利用数组的初始化。利用数值的初始化就是为迭代器指向的链表提供一个初始结点,利用数组的初始化则是从数组中复制数值,让迭代器指向的链表拥有一个初始序列。

值得一提的是,利用迭代器的初始化和“迭代器1=迭代器2”的区别。由于对象的引用特性,后者实际上是一个浅拷贝,迭代器1和迭代器2这两个字面值都指向同一个迭代器,那么对于迭代器2的任何操作,比如指向的结点的改变,都会对迭代器1产生影响。因此,利用迭代器的初始化仅仅是将迭代器2中的pos对象拷贝给迭代器1,这样两个迭代器虽然指向同一个结点,本质上却还是不同的迭代器,因此互不干涉。

public iterator()
{
    pos = new DoubleLinkedNode();
}

public iterator(T obj)
{
    pos = new DoubleLinkedNode(obj);
}

public iterator(iterator<T> it, int id = 0)
{
    pos = pos = findPotion(pos,id);
}

public iterator(T[] tar)//使用数组初始化
{
    if (tar.Length == 0)
    {
        pos = new DoubleLinkedNode();
        return;
    }
    DoubleLinkedNode first;
    DoubleLinkedNode next;
    first = new DoubleLinkedNode(tar[0]);
    pos=first;
    for (int i = 1; i < tar.Length; i++)
    {
        next = new DoubleLinkedNode(tar[i], first, pos);
        first = next;
    }
}

2.1迭代器的自增自减

对于自身pos位置的重设。

public static iterator<T> operator ++(iterator<T> a)
{
    a.pos = a.pos.Next;
    return a;
}

public static iterator<T> operator --(iterator<T> a)
{
    a.pos = a.pos.Last;
    return a;
}

2.2迭代器的加减

和自增自减不同的是,迭代器自身的pos并没有改变,而是返回了一个pos为自身pos经过和加减的数字有关的寻址后获得的新pos的迭代器。这个新的迭代器和原来的迭代器无关,因此可以被放心地赋值给其他空迭代器。

public static iterator<T> operator +(iterator<T> a, int id)
{
    iterator<T> b = new iterator<T>();
    b.pos = findPotion(a.pos, id);
    return b;
}

public static iterator<T> operator -(iterator<T> a, int id)
{
    iterator<T> b = new iterator<T>();
    b.pos = findPotion(a.pos, -id);
    return b;
}

2.3迭代器的下标

迭代器的下标和数组的下标用法相同。

在用作右值时,假如下标中的数字为6,那下标运算就返回迭代器向后寻址6次后指向的结点中的数据。

在用作左值时,由于结点数据是密封只读的,要对数据进行更改,就要在寻址后,在当前指向的结点前插入用新值初始化的新结点,再把原来的结点删除。若删除的就是pos结点,还要把pos重设为新结点。

由于链表中的插入和删除操作都只涉及常数个引用对象的变更,其时间复杂度是很低的。

public T this[int id]
{
    get
    {
        return findPotion(pos, id).Data;
    }
    set
    {
        DoubleLinkedNode next = findPotion(pos, id);
        DoubleLinkedNode newPos = new DoubleLinkedNode(value, next.Last, next);
        if (id == 0)
            pos = newPos;
        next.delete();
    }
}

2.4迭代器的对比

重载函数Equal实现迭代器对比。“迭代器1==迭代器2”和“迭代器1.Equal(迭代器2)”的概念是不同的,前者表示两个迭代器引用的必须是同一个迭代器对象,后者只许两个迭代器引用的迭代器对象指向的是同一个结点即可。举例来说,对于通过public iterator(iterator it)方法初始化的新迭代器,它和原迭代器用==进行对比时返回的结果是false,因为新迭代器和旧迭代器并非同一个迭代器,而Equal方法对比返回的是true,因为他们的pos相同。

public bool Equal(iterator<T> it)
{
    if (pos==it.pos)
        return true;
    return false;
}

3.1用迭代器插入单个元素

插入单个元素仅仅就是将结点自己的前后结点引用修改为要插入的位置的前后结点,在把上一个结点的下一个结点引用修改为插入的元素所在的结点,把下一个结点的上一个结点引用修改为插入的元素所在的结点。

public void add(T tar)
{
    if (!pos.avaluable)
    {
        pos = new DoubleLinkedNode(tar);
        return;
    }
    DoubleLinkedNode first = new DoubleLinkedNode(tar, pos.Last, pos);
}

public void add(T tar, int id)
{
    if (!pos.avaluable)
    {
        pos = new DoubleLinkedNode(tar);
        return;
    }
    DoubleLinkedNode first = findPotion(pos,id);
    DoubleLinkedNode now = new DoubleLinkedNode(tar, first, first.Next);
}

3.2用迭代器插入多个元素

从数组插入多个元素的步骤也很简单:寻址到起点,以起点为last指针、以终点为next指针创建新结点,将新结点设为起点,循环往复。

public void add(T[] tar)
{
    if (tar.Length == 0)
        return;
    int i;
    if (!pos.avaluable)
    {
        i = 1;
        pos = new DoubleLinkedNode(tar[0]);
    }
    else
        i = 0;
    int num = tar.Length, startOfTar = 0;
    DoubleLinkedNode first = pos.Last;
    for (; i < num; i++)
    {
        DoubleLinkedNode now = new DoubleLinkedNode(tar[startOfTar + i], first, first.Next);
        first = now;
    }
}

public void add(T[] tar, int startOfMe, int startOfTar = 0)
{
    if (tar.Length == 0)
        return;
    if (startOfTar < 0)
        return;
    int i;
    if (!pos.avaluable)
    {
        i = 1;
        pos = new DoubleLinkedNode(tar[startOfTar]);
    }
    else
        i = 0;
    int num = tar.Length;
    DoubleLinkedNode first = findPotion(pos,startOfMe);
    for (; i < num; i++)
    {
        DoubleLinkedNode now = new DoubleLinkedNode(tar[startOfTar + i], first, first.Next);
        first = now;
    }
}

public void add(T[] tar, int startOfMe, int startOfTar, int num)
{
    if (tar.Length == 0)
        return;
    if (startOfTar < 0)
        return;
    if (num > tar.Length - startOfTar)
        num = tar.Length - startOfTar;
    int i;
    if (!pos.avaluable)
    {
        i = 1;
        pos = new DoubleLinkedNode(tar[startOfTar]);
    }
    else
        i = 0;
    DoubleLinkedNode first = findPotion(pos, startOfMe);
    for (; i < num; i++)
    {
        DoubleLinkedNode now = new DoubleLinkedNode(tar[startOfTar + i], first, first.Next);
        first = now;
    }
}

3.3链的连接

从迭代器插入元素实际上就是连接两条链。在这个功能的实现中将用到迭代器对链表的遍历,这也是我们使用迭代器操作链表的初衷。

public void add(iterator<T> tar)
{
    if (!tar.pos.avaluable)
        return;
    iterator<T> tar2=new iterator<T>(tar);
    if (!pos.avaluable)
    {
        pos = new DoubleLinkedNode(tar2[0]);
        tar2++;
    }
    DoubleLinkedNode first = pos.Last;
    do
    {
        DoubleLinkedNode now = new DoubleLinkedNode(tar2[0], first, first.Next);
        tar2++;
        first = now;
    } while (!tar2.Equal(tar));
}

public void add(iterator<T> tar, int startOfMe, int startOfTar = 0)
{
    if (!tar.pos.avaluable)
        return;
    iterator<T> tar2 = new iterator<T>(tar+startOfTar);
    if (!pos.avaluable)
    {
        pos = new DoubleLinkedNode(tar2[0]);
        tar2++;
    }
    DoubleLinkedNode first = findPotion(pos,startOfMe);
    do
    {
        DoubleLinkedNode now = new DoubleLinkedNode(tar2[0], first, first.Next);
        tar2++;
        first = now;
    } while (!tar2.Equal(tar +startOfTar));
}

public void add(iterator<T> tar, int startOfMe, int startOfTar,int num)
{
    if (!tar.pos.avaluable)
        return;
    int count=0;
    iterator<T> tar2 = new iterator<T>(tar + startOfTar);
    if (!pos.avaluable)
    {
        pos = new DoubleLinkedNode(tar2[0]);
        tar2++;
        count++;
    }
    DoubleLinkedNode first = findPotion(pos, startOfMe);
    do
    {
        if (count >= num)
            break;
        DoubleLinkedNode now = new DoubleLinkedNode(tar2[0], first, first.Next);
        tar2++;
        count++;
        first = now;
    } while (!tar2.Equal(tar + startOfTar));
}

迭代器对链表的遍历需要先通过迭代器对迭代器的初始化创建一个和起点指向的链表结点相同的迭代器2,之后利用dowhile循环,在每次循环当中让迭代器1自增指向下一个结点,直到用Equal函数判断两个迭代器指向的结点相同。由于链表是环状的,也就是回到了起点,完成了对整个链表的遍历。

使用迭代器对作为数据源的链表进行遍历获取数据的同时,作为目标的链表也使用普通的添加元素的方式储存数据,就完成了从一个链表中将数据插入另一个链表的工作。

4.1删除元素

删除元素的步骤就是先寻址获取需要删除的第一个元素,然后利用指向下一个元素的引用,将自身指向下一个元素,再调用上一个元素的delete方法完成析构,再继续迭代。若上一个结点是pos结点,则要重设pos结点为当前结点。

public void delete(int start = 0)
{
    DoubleLinkedNode now = findPotion(pos, start);
    do
    {
        now = now.Next;
        now.Last.delete();
    } while (now != pos) ;
}

public void delete(int start, int num)
{
    DoubleLinkedNode now = findPotion(pos, start);
    int count = 0;
    while (count<num)
    {
        now = now.Next;
        if (now.Last == pos)
            pos = now;
        now.Last.delete();
        count++;
    }
}

4.2获取子链

获取子链操作和删除操作的不同的在于:

  • 被删除的结点变为不可用,但子链中的结点可用
  • 没有迭代器指向被删除的结点,但有迭代器指向子链
  • 原链的结构不受影响
public iterator<T> sub(int start = 0)
{
    DoubleLinkedNode now = findPotion(pos, start);
    iterator<T> it = new iterator<T>();
    it.pos=new DoubleLinkedNode(now);
    DoubleLinkedNode next = it.pos;
    do
    {
        now = now.Next;
        new DoubleLinkedNode(now,next,it.pos);
        next = next.Next;
    } while (now != pos.Last);
    return it;
}

public iterator<T> sub(int start, int num)
{
    DoubleLinkedNode now = findPotion(pos, start);
    DoubleLinkedNode end = findPotion(now, num);
    iterator<T> it = new iterator<T>();
    it.pos = new DoubleLinkedNode(now);
    DoubleLinkedNode next = it.pos;
    do
    {
        now = now.Next;
        new DoubleLinkedNode(now, next, it.pos);
        next = next.Next;
    } while (now != end);
    return it;
}

4.3栈与队列

链式栈与链式队列的操作,实际上就是返回队头或栈尾结点中的值,再将队头或栈尾结点删除。注意删除队头结点时要重设迭代器的pos成员。

public T pop()
{
    if (!pos.avaluable)
        return default(T);
    T obj = pos.Last.Data;
    pos.Last.delete();
    return obj;
}

public T start()
{
    if (!pos.avaluable)
        return default(T);
    T obj = pos.Data;
    pos = pos.Next;
    pos.Last.delete();
    return obj;
}

5.1查找

由于查找并不能更改原迭代器指向的结点,而是要返回一个指向与查找目标数值相同的结点的新迭代器,在函数当中要利用原迭代器初始化新的迭代器。利用dowhile循环遍历链表,每次将目标数值与目前迭代器指向结点的数值进行对比。

public iterator<T> find(T tar, int start = 0)//从左到右查找元素
{
    iterator<T> target = this + start;
    do
    {
        if (target[0].CompareTo(tar)==0)
            break;
        target++;
    } while (target!=this+start);
    return target;
}

public iterator<T> find(T tar, int start,int end)//从左到右查找元素
{
    iterator<T> target = this + start;
    iterator<T> ending = this+end;
    do
    {
        if (target[0].CompareTo(tar) == 0)
            break;
        target++;
    } while (target != ending);
    return target;
}

public iterator<T> reverseFind(T tar, int start = 0)
{
    iterator<T> target = this + start;
    do
    {
        if (target[0].CompareTo(tar) == 0)
            break;
        target--;
    } while (target != this + start);
    return target;
}

public iterator<T> reverseFind(T tar, int start, int end)
{
    iterator<T> target = this + start;
    iterator<T> ending = this + end;
    do
    {
        if (target[0].CompareTo(tar) == 0)
            break;
        target--;
    } while (target != ending);
    return target;
}

5.2快速排序

双向链表具有双向可迭代性,这与快速排序的需求是一致的。进行快速排序的指针在这里从int型的下标被替换为了两个新建的迭代器,通过迭代器下标取值和下标赋值来实现数据位置的变换。

private void quickSort(iterator<T> s, iterator<T> e)
{
    if (s - 1 != e && s != e)
    {
        iterator<T> i, j;
        T x1, x2;
        i = s;
        j = e;
        x1 = i[0];
        x2 = i[0];
        while (i != j)
        {
            while (i != j && j[0].CompareTo(x1) > 0)
                j--;
            if (i != j)
            {
                i[0] = j[0];
                i++;
            }
            while (i != j && i[0].CompareTo(x1) < 0)
                i++;
            if (i != j)
            {
                j[0] = i[0];
                j--;
            }
        }
        i[0] = x2;
        reQuickSort(s, i - 1);
        reQuickSort(i + 1, e);
    }
}

public void sort()//快速排序
{
    quickSort(this, this-1);
}

private void reQuickSort(iterator<T> s,iterator<T>  e)
{
    if (s-1 != e&&s!=e)
    {
        iterator<T> i, j;
        T x1, x2;
        i = s;
        j = e;
        x1 = i[0];
        x2 = i[0];
        while (i != j)
        {
            while (i != j && j[0].CompareTo(x1) < 0)
                j--;
            if (i != j)
            {
                i[0] =j[0];
                i++;
            }
            while (i!=j && i[0].CompareTo(x1) > 0)
                i++;
            if (i != j)
            {
                j[0] = i[0];
                j--;
            }
        }
        i[0] = x2;
        reQuickSort(s, i - 1);
        reQuickSort(i + 1, e);
    }
}

public void reSort()//快速排序
{
    reQuickSort(this,this-1);
}

结语

写C#链表最大的坑还是引用与对象本身的区别,写的时候必须牢记以下四点:

  • 赋值符号创建同一个对象的不同引用
  • 构造函数可创建部分成员相同的不同对象的不同引用
  • 引用传参传入的引用在函数当中的更改不会影响函数外的该引用
  • 引用传参传入的引用指向的对象在函数当中的更改会影响函数外的该引用指向的对象

猜你喜欢

转载自blog.csdn.net/weixin_43441742/article/details/88776531