数据结构与算法 —— 01 线性表

2017/05/31

1.线性表(Linear List) ——————本质为:"线性表"

特点:具备线性结构的特点,且表中元素属于同一数据对象,元素之间存在一种序偶关系(即有'先后'关系)。
基本操作:

① 初始化
② 插入
③ 删除
④ 查找 (找出特定元素在表中的位置)
⑤ 获取 (获取第 i 个元素)
⑥ 更新(即修改表中数据项的内容)
⑦ 判空
⑧ 遍历度 (即数据表中的数据的个数)
⑩ 销毁
⑨ 求长(整个线性表)
表示形式:
㈠'逻辑结构':

(a1, a2, a3, ..., ai-1, ai, ai+1, ..., an)
可分为有序线性表(递增, 非递增, 递减, 非递减)、无序线性表

㈡'物理存储结构'

(1)顺序存储结构:线性顺序表

即用一组地址连续的存储单元依次存储数据元素。
采用一维数组来描述顺序存储结构,因此存储映像由数组的存储单元组成。

线性顺序表为"空"的条件:length == 0

代码描述:

public class SequenceList<T> {
    private final int maxSize = 10; //顺序表中的数组对象
    private int length;     //保存顺序线性表当前的数据元素个数, 
    private T[] listArray;  //存储元素的数组
    
    /**
    *   1.初始化操作
    *   注意:由于线性顺序表的长度是固定的,初始化时要指定表长。因此,存在两种
    *       初始化方式:1.使用默认的长度
    *                   2.自定义长度
    */
    //构造一个空的线性表,长度(容量)采用默认长度(容量):maxSize
    public SequenceList() {
        length = 0; //线性表初始为空
        //注意,不可以直接实例化泛型对象,所以先实例化一个Object数组
        //然后把它转换为相应的泛型数组                
        listArray = (T[])new Object[maxSize]; 
    }
    ///构造一个空的线性表,长度(容量)采用自定义长度(容量):n
    public SequenceList(int n) {
        //首先的输入的参数进行合法性的检查
        if (n <= 0) {
            System.out.println("error");
            System.exit(1);
        }
        length = 0; //线性表初始为空
        //注意,不可以直接实例化泛型对象,所以先实例化一个Object数组
        //然后把它转换为相应的泛型数组    
        listArray = (T[])new Object[maxSize]; 
    }

    // 2.插入操作 (返回是否插入成功的信息),时间复杂度:O(n)
    public boolean add(T obj, int pos){
        //首先的输入的参数进行合法性的判断: 1<=pos<=length+1
        if (pos < 1 || pos > length + 1) {
            System.out.println("pos值不合法");
            return false;
        }
        /**
        * 注意:由于顺序线性表是基于数组实现的,因此,有表满的问题,需要考虑(链式线性表则无表满的问题)
        * 当前线性表的元素个数等于线性表的存储长度(容量)时,即表已经满了。 
        * 需要对该线性表进行扩容2陪,切勿忘记对原表数据的复制
        */
        if (length == listArray.length) {
            T[] p = (T[]) new Object[length*2];
            for (int i =0; i < length; i++) {
                p[i] = listArray[i];
            }
            listArray = p;
        }
        /**
        *   执行数据插入过程
        *   说明:注意线性表的下标是从1开始的,而数组的下标是从0开始的
        */
        for (int i = length; i >= pos; i--) {
            listArray[i] = listArray[i - 1];
        }
        listArray[pos - 1] = obj; //插入元素
        length ++;  //线性表长度加1
        return true; //插入成功
    }

    // 3.删除操作 (返回被删除的元素),时间复杂度:O(n)
    public T remove(int pos) {
        //首先判空表是否为空
        if (isEmpty()) {
            System.out.println("该顺序表为空,不能执行删除操作");
            return null;
        }
        //对删除位置的合法性进行检查:1<=pos<=length
        if (pos < 1 || pos > length) {
            System.out.println("pos位置不合法");
            return null;
        }
        //执行删除过程
        T x = listArray[pos - 1]; //先将要删除的元素存储起来,否则会被覆盖了
        for (int i = pos; i < length; i++) {
            listArray[i - 1] = listArray[i];
        }
        length --; //线性表长度减1
        return x;   //返回被删除的元素
    }

    // 4.查找操作 (返回元素所在的位置),时间复杂度:O(n)
    public int find(T obj) {
        //判断线性表是否为空
        if (isEmpty()) {
            System.out.println("该顺序表为空,没有任何元素可查询");
            return -1;
        }
        //返回待查找的元素
        for (int i = 0; i < length; i++) {
            if (listArray[i].equals(obj)) {
                return i + 1; //注意:线性表的位置和数组的位置差1                               
            }
        }
        return -1;
    }
    // 5.获取元素操作 (返回获取到的元素)
    public T value(int pos) {
        //先进行判空
        if (isEmpty()) {
            System.out.println("该线性表为空,没有元素可获取");
            return null;
        }
        //进行pos位置合法性的检查
        if (pos < 1 || pos > length) {
            System.out.println("error");
            return null;
        }
        //返回待查找的元素
        return listArray[pos - 1]; //注意减1
    }
    // 6.更新元素内容操作 (返回是否更新成功的信息)
    public boolean modify(T newObj, int pos) {
        //先进行判空
        if (isEmpty()) {
            System.out.println("该线性表为空,没有元素可更新");
            return false;
        }
        //进行pos位置合法性的检查
        if (pos < 1 || pos > length) {
            System.out.println("error");
            return false;
        }
        //元素内容更新
        listArray[pos - 1] = newObj;
        return true;
    }
    // 7.判空操作 (返回是否为空的信息)
    public boolean isEmpty() {

        //当length为0,则表明该线性表为空
        return length == 0;
    }
    // 8.求长度 (返回线性表中的数据的个数)
    public int size() {
        //内部变量length就是记录了线性表当前的元素个数
        return length;
    }
    // 9.正向遍历数据表
    public void nextOrder() {
        System.out.print("[");
        for (int i = 0; i < length; i++) {
            if (i == length - 1) {
                System.out.println(listArray[i] + "]");
            } else {
                System.out.print(listArray[i] + ", ");
            }
            
        }
    }
    // 10.销毁线性顺序表
    public void clear() {
        //将该线性表的元素个数(length)修改为0,即可达到销毁线性表的效果
        length = 0;
    }

}

(2) 链式存储结构:单链表、循环链表、双向链表
采用一组'任意(可以连续也可以不连续)'的存储单元来存放线性表的数据元素。

1."单链表"(带头结点)
由于在物理上存储位置和逻辑顺序不一致,因此,必须在存放某一元素时还须存储一个指示其直接“后继存放位置”(即后继结点)的指针。因此,每个数据元素对应的“存储映像”(又称为结点)由两部分组成:数据域指针域
  ┌───────┬───────┐
结点(Node): │ 数据域│ 指针域│
└───────┴───────┘

含有头结点的单链表:
head——> 头结点(数据域通常无意义)——> 结点1——> 结点2——> ... 结点n(指针域为null)

单链表为空的条件:head.next == null
到达表尾的条件:p.next == null

头指针头结点的联系与区别
头指针:
指向链表第一个结点(当没有头结点存在时)的指针
头指针具有标识作用,常用它作为链表的名字
无论链表是否为空,头指针均不为空(即头指针是必须存在的)
头指针是链表的必要元素
头结点:
头结点的存在是为了操作(如对第一个结点进行增、删等操作时)的统一和方便
头结点放在第一个结点的前面,其数据域通常无意义(有时可以用来存放链表的长度)
头结点不是链表的必要元素(可有可无)

注意:"单链表"(又称为线性链表)是重要考察的

代码描述:
1)"结点"代码描述:

/**
*   分为两部分:数据域、指针域
*/
public class Node<T> {
    T data;
    Node<T> next; //因为指针就是一个地址,代表下一个Node的地址。
    //该构造器表示只初始化了指针域,数据域不存储有效的用户数据
    public Node(Node<T> n) {
        next = n;
    }
    //数据域和指针域均初始化
    public Node(T obj, Node<T> n) {
        data = obj;
        next = n;
    }

    public T getData() {
        return data;
    }

    public Node<T> getNext() {
        return next;
    }
}

2)"单链表"代码描述:

public class LinkList<T> {
    private Node<T> head; //头指针
    private int length; //单链表的长度(即元素个数)
    /**
    *   1.初始化
    *   链表的初始化不需要在开始就确定其存储长度,因为它的长度是可变的
    *   因此,链表的初始化就只有一种方式
    */
    public LinkList() {
        length = 0; //初始化线性链表为空
        //只初始化了该结点的指针域,因此这个结点是头结点,而非第一个结点
        head = new Node<T>(null); // 让head指向头结点
    }
    //2.获取链表头结点的地址
    public Node<T> getHead() {
        return head;
    }
    /**
    *   3.插入元素:指在第pos-1和第pos个结点之间插入新的结点。
    *   时间复杂度:O(n)
    */
    public boolean add(T obj, int pos) {

        //先对插入位置的参数合法性进行检查:1<=pos<=length+1
        if (pos < 1 || pos > length + 1) {
            System.out.println("pos参数不合法");
            return false;
        }
        /**
        * 注意:由于线性链表没有表满的问题,则不需要考虑扩容的问题(线性顺序表则要考虑表满的问题)
        * 先寻找到pos位置所对应的结点
        *   由于线性链表不具有随机访问(即任意访问)的特点,所以插入
        *   数据前,必须从头到插入点顺序扫描每个结点,从而找到待插入
        *   位置的结点。这点是不同于线性顺序表(可以随机找到某位的元素)
        */
        int num = 1;//辅助寻找pos位置的
        Node<T> p = head;
        Node<T> q = head.next;
        while(num < pos) {
            p = q;
            q = q.next;
            num ++;
        }
        /**
        *   此时p指向pos-1位置的结点,q指向pos位置的结点.
        *   要往p和q之间插入新的元素,则先将数据obj封装进Node结点中
        */
        p.next = new Node<T>(obj, q); //这句话好好体会
        length ++;
        return true;
    }
    /**
    *   4.删除元素:指删除线性链表的第pos个结点
    *   时间复杂度:O(n)
    *   
    */
    public T remove(int pos) {
        //先对链表进行判空
        if (isEmpty()) {
            System.out.println("链表为空,不可以进行删除操作");
            return null;
        }
        //再对删除位置的参数合法性进行检查:1<=pos<=length
        if (pos < 1 || pos > length) {
            System.out.println("pos参数不合法");
            return null;
        }
        /**
        * 先寻找到pos位置所对应的结点
        *   由于线性链表不具有随机访问(即任意访问)的特点,所以插入
        *   数据前,必须从头到插入点顺序扫描每个结点,从而找到待插入
        *   位置的结点。这点是不同于线性顺序表(可以随机找到某位的元素)
        */
        int num = 1;
        Node<T> p = head;
        Node<T> q = head.next;
        while (num < pos) {
            p = q;
            q = q.next;
            num ++;
        }
        /**
        *   此时p指向pos-1位置的结点,q指向pos位置的结点.
        *   要删除q指向的元素
        */
        p.next = q.next;
        length --;
        return q.data;                          
    }
    //5.获取元素:获取第pos位置处的结点对应的元素
    public T value(int pos) {
        //先对链表进行判空
        if (isEmpty()) {
            System.out.println("链表为空,不可以进行获取操作");
            return null;
        }
        //再对获取位置的参数合法性进行检查:1<=pos<=length
        if (pos < 1 || pos > length) {
            System.out.println("pos参数不合法");
            return null;
        }
        //进行"获取"过程:从头至尾进行搜索
        int num = 1;
        Node<T> q = head.next; //q的初始位置指向第一个结点处
        while (num < pos) {
            q = q.next;
            num ++;
        }
        //此时搜索至pos位置处
        return q.data;                          
    }
    //6.查找元素
    public int find(T obj) {
        //先对链表进行判空
        if (isEmpty()) {
            System.out.println("链表为空,不可以进行查找操作");
            return -1;
        }
        //进行查找过程:从头到尾挨个比较待查找元素和链表中元素是否相等
        int num = 1;
        Node<T> q = head.next; //q的初始位置指向第一个结点处
        while (q != null) {
            if (q.data.equals(obj) == false) {
                //表明还没有查找到
                q = q.next;
                num ++;                                 
            }else {
                break;
            }               
        }
        //此时表明已经查找到或者搜索至尾仍没有查找到,所以要进行判断是哪种情况
        if (q == null) {
            //表明搜索至尾仍没有查找到
            return -1;
        }
        //此时肯定是查找到了
        return num;                         
    }
    //7.修改元素
    public boolean modify(T newObj, int pos) {
        //先对链表进行判空
        if (isEmpty()) {
            System.out.println("链表为空,不可以进行修改操作");
            return false;
        }
        //再对获取位置的参数合法性进行检查:1<=pos<=length
        if (pos < 1 || pos > length) {
            System.out.println("pos参数不合法");
            return false;
        }

        //进行修改过程
        int num = 1;
        Node<T> q = head.next; //q的初始位置指向第一个结点处
        while (num < pos) {
            q = q.next;
            num ++;
        }
        //此时已经找到pos处,q指向pos处的结点
        q.data = newObj;
        return true;
    }
    //8.判空
    public boolean isEmpty() {
        return length == 0;
    }
    //9.求长度 (返回线性链表中的数据的个数)
    public int size() {
        return length;
    }
    //10.正向遍历线性链表
    public void nextOrder() {
        System.out.print("[");
        Node<T> q = head.next;
        int num = 1;
        while (q != null) {
            if (num == length) {
                System.out.println(q.data + "]");
                break;
            }
            System.out.print(q.data + ", ");
            q = q.next;
            num ++;         
        }
    }
    //11.销毁线性链表
    public void clear() {
        length = 0;
        head.next = null;//把头结点的指针域置为空
    }
}

2."循环链表"
该类链表最后一个节点的指针域不再为空,而是指向表头结点,使整个链表形成一个环这样,可以从表中任意地方出发均可到达其他结点(这点不同于单链表:必须从表头head去访问),这是循环链表最大的优点处。

'循环链表的标记方式'(2种):
(1)用头指针head标记:
_____________________________________________________
↓ |
head——> 头结点(数据域通常无意义)——> 结点1——> 结点2——> ... 结点n(指针域为head.next)

判断链表为空的条件: head.next == head
到达表尾的条件:p.next == head
到达表头的时间复杂度:O(1)
到达表尾的时间复杂度:O(n)

(2)用尾指针rear标记:
如果,将循环链表的标识'头指针'从头结点处移至'尾结点'处,形成"尾指针",用rear来表示。
则到达表头、表尾都很方便。
____________________________________________________
↓ |
'头结点(数据域通常无意义)——> 结点1——> 结点2——> ... 结点n(指针域为head.next) <—— rear'

判断链表为空的条件: head.next == head
到达表尾的条件:p.next == head
到达表头的时间复杂度:O(1)
到达表尾的时间复杂度:O(1)

3."双向链表"
单向链表和循环链表找后继结点的时间复杂度均为:O(1),而找前驱结点的T[n]=O(n)若采用双向链表的方式,则到其后继结点和前驱结点的T[n]均为:O(1),但为此付出了空间上的代价:为每个元素结点增加了前驱指针域,prior

                    ┌───────┬───────┬──────┐

双向链表结点(DuNode): │ prior │ data │ next │
└───────┴───────┴──────┘

注意:'双向循环链表'

代码描述:
1)"双向链表结点"描述:

public class DuNode<T> {
    T data;
    DuNode<T> prior;
    DuNode<T> next;
    public DuNode(DuNode<T> n) {
        next = n;
        prior = null
    }

    public DuNode(T obj, DuNode<T> n, DuNode<T> p) {
        data = obj;
        next = n;
        prior = p;
    }
}

2)"双向链表"描述(伪代码):

    public class DuLinkList<T> {
        //插入操作:...a, p...之间插入s(注意顺序)
        s.prior = p.prior
        p.prior.next = s
        s.next = p
        p.prior = s

        //删除操作:...a, p, s...删除p
        p.prior.next = p.next
        p.next.prior = p.prior
    }

4."静态链表"
用一组数组(存储地址连续)来实现的链表
┌────┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│游标│ 6 │ 5 │ 3 │ 4 │'0'│ 2 │ │ │ │ │ │ │ 1 │
├────┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
│数据│空 │ A │ C │ D │ E │ B │ │ │ │ │ │ │ │
├────┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
│下标│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10│...│999│
└────┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
1)第一位置的游标总是指向链表中为空的存储单元(这些为空的单元也称之为'备用链表')的下标,且其数据域中不存储数据
2)最后一个有数据的单元的游标为0(即是指向第一个存储单元的)
3)数组最后一个元素的游标指向第一个有数据的存储单元

静态链表的优缺点:
优点:
1.在插入和删除时,只需修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点

缺点:
1.没有解决连续存储分配(即数组的分配)带来的表长难以确定的问题
2.失去了顺序存储结构随机存取的特性。

言而总之:静态链表是为了给没有指针的编程语言设计的一种实现单链表功能的方法
虽然现在大多数编程语言不需用静态链表了,但这种思路很值得思考学习

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容