数据结构——链表

1.什么是链表

 链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

2.节点

节点维护变量data和next,分别用于存储数据和指向下一个节点。

C#:

class Node<T>
{
    private T data;
    private Node<T> next;

    public T Data
    {
        get { return data; }
        set { data = value; }
    }

    public Node<T> Next
    {
        get { return next; }
        set { next = value; }
    }

    public Node(T item)
    {
        data = item;
    }

    public Node(T item, Node<T> nextNode)
    {
        data = item;
        next = nextNode;
    }
}

 Python:

class Node:
    def __init__(self, data, next=None):
        self.data = data
        self.next = next

    def __repr__(self):
        return str(self.data)

3.链表类

正式构建一个链表类

链表类应该具有以下属性或方法:

count 元素数量
head 头节点指针
insert 向指定位置插入元素
get/set 获取/修改节点值
remove 删除指定元素

 

定义好一个类:

C#:

class LinkList<T>:
{
    private Node<T> head;//头节点
    private int count;//计数器
}

Python:

class LinkList:
    def __init__(self):
        self._head = None
        self.count = 0

3.1 获取链表元素个数

由于我们维护了一个属性,所以直接返回count即可

class LinkList<T>:
{
    ...
    public int Count { get { return count; } }
}

在Python中我们重写__len__方法

def __len__(self):
    return self.count

3.2 获取指定位置节点

类比索引功能,设定头节点位置为0,下一节点位置+1

实现一个函数,返回指定位置的Node对象,两种直观解决思路:

迭代,通过一个while或循环来遍历至指定位置

递归,链表是一种天然适合递归的数据结构,通过递归也可以轻松找到目标节点

C#(迭代实现):

    private Node<T> GetIndexNode(int index)
    {
        //获取指定索引的Node对象
        if (index > count)
        {
            throw new IndexOutOfRangeException();
        }

        Node<T> target = head;
        for (int i = 0; i < index; i++)
        {
            target = target.Next;
        }

        return target;
    }

Python(递归实现):

def _getnode(self, index: int):
    # 返回指定索引的节点
    def getnode(node: Node, offset: int):
        '''递归函数
        :param node: 节点
        :param offset: 偏移量
        :return:
        '''
        if not node or offset < 0:
            raise IndexError('超出索引范围')
        if offset == 0:
            return node
        return getnode(node.next, offset-1)
    return getnode(self._head, index)

 Tips:使用递归实现可能会出现栈溢出,python默认调用栈容量1000

3.3 向指定位置插入一个节点

向位置i插入一个节点,只要找到目标位置前一个节点(i-1),把i-1节点的next指向新节点,再把新节点的next节点指向i+1节点即可

特别要注意如果向0位置插入节点,要修改head的指向

C#:

    public void Insert(T item, int i)
    {
        if (i == 0)
        {
            Node<T> newNode = new Node<T>(item,head);
            head = newNode;
        }
        else
        {
            Node<T> preNode = GetIndexNode(i - 1);
            Node<T> newNode = new Node<T>(item, GetIndexNode(i));
            preNode.Next = newNode;
        }

        count++;
    }

Python:

def insert(self, item, index):
    if index > self.size:
        raise IndexError
    if index == 0:
        node = Node(item, self._head)
        self._head = node
    else:
        prev = self._getnode(index - 1)
        node = Node(item, prev.next)
        prev.next = node
    self.count += 1

3.4 访问/修改一个节点的值

前面已经实现了查找节点的访问,现在只用提供一个接口get/set节点存储的值即可

C#:

public T GetItem(int i) {  return GetIndexNode.Data; }

Python:

def __getitem__(self, item):
    if not isinstance(item,int):
        raise IndexError
    return self._getnode(item).data

def __setitem__(self, key, value):
    if not isinstance(key,int):
        raise IndexError
    node = self._getnode(key)
    node.data = value

3.5 删除节点

实现pop和remove两个方案,分别使用索引位置和节点值来删除一个节点

C#:

    public T Pop(int i)
    {

        if (i == 0)
        {
            Node<T> target = head;
            head = target.Next;
            count--;
            return target.Data;
        }

        T response = GetIndexNode(i).Data;
        Node<T> preNode = GetIndexNode(i - 1);
        Node<T> nextNode = GetIndexNode(i).Next;
        preNode.Next = nextNode;
        count--;
        return response;
    }

    public void Remove(T item)
    {
        if (head.data = item) {
            head = head.next;
        } else {
            Node<T> prev = Node(None,head);
            while (prev.next != None) {
                Node<T> cur = prev.next;
                if (cur.data == item) {
                    prev.next = cur.next;
                    break;
                }
            }
        count--;
    }

Python:

def pop(self, index: int):
    # 根据索引位置删除链表节点
    prev = self._getnode(index - 1)
    target = prev.next
    prev.next = target.next
    self.count -= 1
    return target.data

def remove(self, item):
    # 根据节点值删除链表节点
    prev = Node(None, self._head)
    while prev.next != None:
        if prev.next.data == item:
            self.count -= 1
            prev.next = prev.next.next
            return
        prev = prev.next

4 性能分析和优化思路

实际上,我们实现的增删改查大多基于GetIndexNode方法,时间复杂度随元素的位置而定,如果操作在链表头,那么时间复杂度O(1),如果操作在链表尾,时间复杂度则O(n),整体来看增删改查的时间复杂度均为O(n)。

与数组对比,由于数组是一个在内存连续存放的数据结构,所以数组支持随机访问(任意索引访问时间复杂度均为O(1))。在随机读写性能上数组会比链表有更好的表现,但是链表也有优点,链表是一个实现了自动扩容的数据结构,我们完全可以不去关心一个链表能容纳多少元素,而数组则往往来通过扩容来实现动态数组,也会造成空间和时间的浪费。

实际上,链表一般只在尾部添加元素,我们完全可以再维护一个foot变量指向链表尾来优化效率。

5. 其他

上面我们实现了一个最基础的链表,实际上链表有不同的许多形式,还有双向链表、循环链表等等,这些链表都是基于单向链表发展出来的,有兴趣的朋友可以亲自一一实现。

posted @ 2019-11-11 00:25  秋叶红了  阅读(5685)  评论(0编辑  收藏  举报