ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

《大话数据结构》学习笔记——第三章 线性表

2020-11-01 02:31:27  阅读:158  来源: 互联网

标签:结点 线性表 int 大话 元素 链表 数据结构 public


第三章 线性表

3.1 开场白

3.2 线性表的定义

线性表(List):零个或多个数据元素的有限序列。线性表逻辑结构类型是线性结构。物理结构可以是顺序存储,也可以是链式存储。线性表元素之间是有顺序的。如下图,ai-1是ai的直接前驱,ai+1是ai的直接后继

3.3 线性表的抽象数据类型


如下即是线性表抽象数据类型的在C#中的实现,这里展示的是metadata,因此没有具体方法细节。

public class List<T> : IList<T> ...
{
    public List();
    public List(int capacity);
    public List(IEnumerable<T> collection);

    public T this[int index] { get; set; }
    public int Count { get; }
    public int Capacity { get; set; }
    public void Add(T item);
    public void AddRange(IEnumerable<T> collection);
    ...
}

合并两个线性表

public static void union(ref List<int> a, List<int> b)
{
    /*   将所有在b线性表中但不在a中的元素插入到a中      */
    for(int i = 0; i < b.Count; i++)
    {
        if (!a.Contains(b[i]))
        {
            a.Add(b[i]);
        }
    }
}

3.4 线性表的顺序存储结构

(1)线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。

(2)顺序存储方式:先在内存中占领一片内存空间,把数据中的第一个元素放在内存地址的第一个位置,然后相邻的数据元素依次放在相邻地址的位置上。C#的数组底层也是这样,初始化时要先给定数组长度,长度分配好了即在内存堆中划分好了一片连续的内存空间,以后不能修改数组的长度,然后可以将元素依次放入数组中。

(3)数据长度和线性表长度区别:数据长度是指占用了内存空间的数据元素数量,有些内存空间可以空着。而线性表长度是指整个分配好的内存空间的长度,包括空着的内存空间。

在C#这个高级语言中,数组是经过额外封装的(?),在初始化时若没有给定数据,则C#会自动把默认值放到数组中,如int的默认值为0,string的默认值为null。

public static void method1()
{
    int[] c = new int[4];
    c[0] = 1;
    c[2] = 3;
    foreach (int i in c) Console.Write("{0},", i);
    Console.WriteLine();
    Console.WriteLine(c.Length);
    string[] d = new string[4];
    d[1] = "123";
    foreach (string i in d) Console.Write("{0},", i);
}

(4)地址计算方法

LOC(ai) = LOC(ai-1) + c;

LOC(ai) = LOC(1) + (i - 1) * c;

通过这个公式,可以随时计算出任意位置的地址,因此线性表的存取时间复杂度为O(1)。

顺序存储结构的插入和删除

(1)获得元素操作:略

(2)插入操作,插入算法的思路:

  • 如果插入位置不合理,抛出异常

  • 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量

  • 从最后一个元素开始向前遍历到第i个位置,分别将他们都想后移一个位置

  • 将要插入的元素放在i的位置

  • 表加长1

C#数组缺数据长度属性,集合List没有最长容量属性,都不太适合实现这个算法,因此直接如下书中的源代码:

(3)删除操作,类似插入操作。

插入操作和删除操作的最坏情况时间复杂度是O(N),最优情况是O(1),平均时间复杂度为O((N-1)/2) = O(N),因此这两个操作的时间复杂度就是O(N)。

(4)顺序存储结构的优缺点

3.6 线性表的链式存储结构

为解决顺序存储方式的缺点,分析:

  • 因为数据元素之间存储器的位置也是相邻的,中间没有空隙,因此插入和删除要挪位置

  • 能不能中间空几个位置预留给插入和删除操作呢?这种方案存在留空位的数量不定,消耗大量空间等问题

  • 那就不要留空位了,所有数据元素随便坐在任意存储空间中,只要让每个数据元素知道下一个数据元素的存储单元地址就行。

(1)线性表的链式存储结构定义:使用任意一批存储单元来存储线性表中的数据,每个存储单元存储了本数据元素的值,还存储了下一个数据元素的存储单元地址。把存储数据元素信息的域称为“数据域”,把存储直接后继位置地址的域称为“指针域”,因此这个直接后继位置地址称为指针

这两部分信息组成数据元素ai的存储映像,称为结点(Node)(Node在数据结构和算法中很常用)。n个节点通过指针连接起来成一个链表,即为线性表的链式存储结构。当前每个结点只有一个指针域,因此也称单链表

C#中链表的结点的结构如下:

class LinkNode: IDisposable
{
    public int val { get; set; } // 自身的值
    public LinkNode next { get; set; } // next属性(指针)
    public LinkNode() { } // 无参数的构造函数
    public LinkNode(int x) { val = x; } // 带参数的初始化函数
    public void Dispose() { }
}

3.7 单链表的读取

在计算机内部,获取某一个数据元素的值,要先获取其物理地址,再找到该物理地址存储空间上存储的值。单链表读取第i个元素,由于其每个数据元素的物理地址信息都存储在上一个数据元素中,因此必须让指针从头节点开始向后移,一共移动i次,即可找到第i个元素的物理地址,再获取其值,当移动i次之前就到达了链表的末尾,说明第i个元素不存在。因此其时间复杂度为O(N)。

3.8 单链表的插入和删除

对于在某个位置插入和删除元素的操作,首先要定位为该元素,时间复杂度为O(N),然后执行指针的转移,时间复杂度为O(1),因此总体时间复杂度为O(N)。

插入:

LinkNode s, p;
s.next = p.next;
p.next = s;

删除:

LinkNode s, p;
p.next = s.next;
s.next = null; // 将要删除的节点的指针指向nullptr,或者回收这个结点,或者什么都不做

3.9 单链表的整表创建

(1)头插法:每次将新节点插入到头结点和第一个结点中间

// 头插法,使用数组创建一个链表,返回链表的头结点,注意头节点不是第一个结点。
public static LinkNode CreateListHead(int[] data)
{
    int data_len = data.Length;
    LinkNode p = new LinkNode();
    for(int i = data_len - 1; i >= 0; i--)
    {
        LinkNode temp = new LinkNode(data[i]);
        temp.next = p.next;
        p.next = temp;
    }
    return p;
}

(2)尾插法:每次将新节点插入到链表尾部

// 尾插法,使用数组创建一个链表,返回链表的头结点
public static LinkNode CreateListTail(int[] data)
{
    int data_len = data.Length;
    LinkNode p = new LinkNode();
    LinkNode temp = p;
    for (int i = 0; i < data_len; i++)
    {
        LinkNode temp2 = new LinkNode(data[i]);
        temp.next = temp2;
        temp = temp2;
    }
    return p;
}

3.10 单链表的整表删除

// p为链表的头结点,清楚整个链表
public static void ClearList(LinkNode p)
{
    LinkNode start = p.next; // 第一个结点
    while (start != null)
    {
        p.next = start.next;
        start.next = null;
        start.Dispose();
        start = p.next;
    }
}

3.11 线性表的顺序存储方式和链式存储方式的优缺点比较

  • 需要频繁地插入或删除数据时,使用单链表更好;若需要频繁地查找数据,而不需要太多的插入删除操作时,使用顺序存储结构更好。

  • 知道数据长度并长度很少变化时,使用顺序存储结构更好;数据长度未知,且将来数据量可能会有较大的变化时,使用单链表更好。

3.12 静态链表

(1)静态链表概念:即把链表存储在数组中的一种结构。然而数组中存储的元素不只是单个数据,而存储了数据游标,这些每个元素存储的游标表示其后继元素所在的数组下标,所以游标即指针。由于数组是预先设置好长度的,因此当链表长度不足数组长度时,就有一部分结点的数据域为空,这部分结点就是备用链表。此外静态链表还会对头元素和尾元素做特殊处理,头元素的数据域为空,游标存储了备用链表的第一个结点的下标;尾元素的数据域也为空,游标存储了第一个数据域不为空的结点的下标。

class StaticLinkNode
{
    public int val { get; set; } // 自身的值
    public int next { get; set; } // next属性(指针)
    public StaticLinkNode() { }
    public StaticLinkNode(int x) { val = x; } // 初始化函数
}
class Charpter3
{
    public static int MAX_LEN = 1000; // 假设数组最长为1000
    public static StaticLinkNode[] StaticLink = new StaticLinkNode[MAX_LEN];
    public static void StaticLinkInit(int n)
    {
        // 数组头尾有特殊用处,n最大为MAX_LEN - 2
        if (n > MAX_LEN - 2 || n < 0)
        {
            throw new Exception("初始化错误,链表长度溢出或链表长度不能为负值");
        }
        // 第一个元素
        StaticLink[0].next = n + 1;
        // 链表数据和游标初始化
        for (int i = 1; i <= n; i++)
        {
            StaticLink[i].val = i; // 这里把值设置简单设置为i
            if (i != n)
            {
                StaticLink[i].next = i + 1;
            }
            else
            {
                StaticLink[i].next = 0;
            }
        }
        // 备用链表游标初始化
        for (int i = n + 1; i < MAX_LEN - 1; i++)
        {
            StaticLink[i].next = i + 1;
        }
        // 最后一个元素
        StaticLink[MAX_LEN - 1].next = 1;
    }
}

(2)静态链表插入元素:

假设在链表第i个元素的位置插入元素e:

  • 取出备用链表的第一个结点(假设下标为u),如果该节点为数组最后一个特殊元素,说明备用链表没有空间,插入失败。

  • 校验插入位置i,如果小于1或大于u,则插入失败;u为第一个备用链表的元素下标。

  • 设置u结点数据域的值。

  • 将数组第一个元素的游标指向u结点的游标。

  • 找到第i - 1个元素的下标k,将u结点的游标指向k结点的游标。

  • 将k结点的游标指向u。(注意最后三步的顺序!!)

public static void StaticLinkInsert(int i, int e)
{
    int u = StaticLink[0].next;
    if (u > MAX_LEN - 2) throw new Exception("数组长度溢出,无法插入元素");
    if (i < 1 || i > u) throw new Exception("插入元素失败,链表长度越界或插入位置为负");
    StaticLink[u].val = e;
    // 链表第i个元素不一定是数组第i+1个元素,因此要用链表的方式遍历到第i个元素取出
    int k = MAX_LEN - 1; // 链表第一个元素的下标存储在数组最后一个元素的游标里面
    // 找到链表第i个结点前面的那个元素的下标为k
    for (int j = 1; j <= i - 1; j++) k = StaticLink[k].next;
    StaticLink[0].next = StaticLink[u].next;
    StaticLink[u].next = StaticLink[k].next;
    StaticLink[k].next = u;
}

(3)静态链表删除元素

假设删除链表第i个元素(假设其下标为j):

  • 校验i,如果小于1或大于MAX_LEN-2,则删除操作失败。

  • 遍历i-1次,找到链表中第i-1个元素(假设其下标为k),将其游标指向第i个元素的游标。

  • 将链表第i个元素数据域清空,将其指针指向数组下标为0的元素的游标。

  • 将数组下标为0的元素的游标指向k。

public static void StaticLinkDelete(int i)
{
    int j = 1, k = 1;
    if (i < 1 || i > MAX_LEN - 2) throw new Exception("删除元素失败,链表长度越界或删除位置为负");
    for (j = 1; j <= i - 1; j++) k = StaticLink[k].next;
    j = StaticLink[k].next;
    StaticLink[k].next = StaticLink[j].next;
    StaticLink[k].next = StaticLink[0].next;
    StaticLink[0].next = k;
}

3.13 循环链表

(1)循环链表概念:将单链表中终端结点的指针由空指针变为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表。循环链表不一定要有头结点,只是为了使空链表和非空链表处理一致,通常加入一个头结点

继续改造:不用头指针表示链表,而使用一个指向尾结点的新指针来表示链表,这样查找尾结点的时间复杂度也是O(1),没有那个新指针之前查找尾结点的时间复杂度为O(N)

3.13 双向链表

(1)双向链表概念:是指在单链表中的每个结点,再设置了一个指向前驱结点的指针域。这样每个结点不仅存储了后继结点的地址信息,还存储了前驱结点的地址信息。其插入元素和删除元素的操作略。

3.15 总结

标签:结点,线性表,int,大话,元素,链表,数据结构,public
来源: https://www.cnblogs.com/yuyr757/p/13888309.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有