Data structure review (1) to the circular queue

Chapter 1 Introduction to Data Structures

Data structure: is a collection of data elements that have one or more specific relationships with each other.

 Chapter 2 Algorithms

Algorithm: A description of the steps to solve a particular problem, represented in a computer as a finite sequence of instructions, with each instruction expressed as one or more operations.

Algorithms have five basic properties: input, output, finiteness, confirmation, and feasibility.

Input and output: The algorithm has zero or more inputs; at least one or more outputs, and the algorithm must have an output.

Finiteness : The algorithm ends automatically without an infinite loop after executing a finite number of steps, and each step is completed within an acceptable time.

Deterministic: Each step of the algorithm has a definite meaning without ambiguity. Under certain conditions, the algorithm has only one set of execution paths, and the same input can only have a unique output result. Each step of the algorithm is precisely defined without ambiguity.

Feasibility: Each step of the algorithm must be feasible, that is, each step can be completed by executing a finite number of times.

Correctness: The correctness of an algorithm means that the algorithm should at least have input, output and processing without ambiguity, and can correctly reflect the requirements of the problem. able to get the correct answers to the questions.

The "correctness" of an algorithm usually varies greatly in usage, and can be roughly divided into four levels.

        1. The algorithm program has no grammatical errors.

        2. Algorithm programs can produce output results that meet the requirements for legal input data.

        3. Algorithm programs can obtain results that meet specifications for illegal input data.

        4. Algorithm programs have satisfactory output results for carefully selected and even difficult test data.

In most cases, the correctness of the algorithm cannot be proved by programs, but by mathematical methods.

Readability: Another purpose of algorithm design is to facilitate reading, understanding and communication.

Robustness: When the input data is illegal, the algorithm can also deal with it instead of producing abnormal or inexplicable results.

High time efficiency (the execution time of the algorithm) and low storage capacity: A good algorithm should also have the characteristics of high time efficiency and low storage capacity.

Measuring Method of Algorithm Efficiency
post hoc statistical method This method is mainly to use the computer timer to compare the running time of the programs compiled by different algorithms through the designed test program and data, so as to determine the efficiency of the algorithm.
Pre-analyzing and estimating method

Before the computer program is compiled, the algorithm is estimated according to the statistical method.

The time it takes for a program written in a high-level programming language to run on a computer depends on the following factors:

        1. The strategy and method adopted by the algorithm.

        2. The code quality generated by compilation.

        3. The input scale of the problem.

        4. The speed at which a machine executes instructions.

The running time of a program depends on the quality of the algorithm and the input size of the problem. The so-called problem input scale is the amount of input.

The most reliable way to measure run time is to count the number of executions of the basic operation that consumes run time. Run time is directly proportional to this technique.

When analyzing the running time of a program, it is most important to think of the program as an algorithm or sequence of steps that is independent of the programming language.

In analyzing the running time of an algorithm, it is important to relate the number of primitive operations to the size of the input, ie the number of primitive operations must be expressed as a function of the size of the input.

When the input size n is unlimited, as long as it exceeds a value N, this function is always greater than another function, we say that the function grows asymptotically.

Asymptotic growth of functions: Given two functions f(n) and g(n), if there exists an integer N such that f(n) is always greater than g(n) for all n>N, then we say that f(n) grows asymptotically faster than g(n).

When judging the efficiency of an algorithm, the constants and other secondary terms in the function can often be ignored, and more attention should be paid to the order of the main term (the highest order term).

An algorithm, as n increases, it will be more and more better than another algorithm, or worse and worse than another algorithm. (Theoretical Basis of Ex-ante Estimation Method)


Algorithm Time Complexity

Algorithm time complexity definition : When performing algorithm analysis, the total execution times T(n) of the statement is a function of the problem scale n, and then analyze the variation of T(n) with n and determine the magnitude of T(n). The time complexity of the algorithm, that is, the time measure of the algorithm, is recorded as: T(n) = O(f(n)). It means that as the problem scale n increases, the growth rate of the algorithm execution time is the same as the growth rate of f(n), which is called the asymptotic time complexity of the algorithm, or time complexity for short. where f(n) is some function of the problem size n.

The notation of using capital O() to reflect the time complexity of the algorithm is called big O notation .

In general, as n increases, the algorithm with the slowest growth of T(n) is the optimal algorithm.

O(1) is called constant order, O(n) is called linear order, O(n^2) is called square order

Derive the big O order method:

        1. Replace all additive constants in run time with constant 1. (No constant items are not considered)

        2. In the modified number of runs function, only the highest order term is kept. (Example: (n^2+n)/2, only keep the highest order item, keep n^2/2)

        3. If the highest order term exists and is not 1, remove the constant multiplied by this term. (Example n^2/2, remove the constant multiplied by this item, the final time complexity of this code is O(n^2)

The result is big O order.

constant order
int sum = 0,n = 100;/*执行一次*/
sum = (1+n)*n/2; /*执行一次*/
printf("%d",sum); /*执行一次*/

The number of runs of this algorithm is f(n)=3. According to the method of deriving the big O order, the first step is to change the constant term 3 to 1. When the highest order item is retained, it is found that it has no highest order item at all, so the time complexity of this algorithm is O(1).

This kind of algorithm that has nothing to do with the size of the problem (the number of n) and has a constant execution time is called a time complexity of O(1), also called a constant order.

For the branch structure, whether it is true or false, the number of executions is constant and will not change as n increases, so the time complexity of the simple branch structure (not included in the loop structure) will also be O(1).

linear order

To analyze the complexity of the algorithm, the key is to analyze the operation of the loop structure.

int i;
for(i=0;i<n;i++){
    /*时间复杂度为O(1)的程序步骤序列*/
}

The time complexity of the above code loop is O(n), because the code in the loop body needs to be executed n times.

logarithmic order
int count = 1;
whhile(count < n){count = count * 2;
/*时间复杂度为O(1)的程序步骤序列*/
}

Because every time the count is multiplied by 2, it is one point closer to n. That is to say, how many 2s are greater than n after being multiplied, will exit the loop. By 2^x = n, the time complexity of this loop is O(log n)

square order
int i,j;
for(i = 0;i < n;i ++){
for(j=0;j<n;j++){
/*时间复杂度为O(1)的程序步骤序列*/
}}

The time complexity of this code is O(n^2)

The time complexity of a loop is equal to the complexity of the loop body multiplied by the number of times the loop runs.

common time complexity
Execution count function order informal term
12 O(1) constant order
2n+3 O(n) linear order
3n^2+2n+1 O(n^2) square order
\log_{2}n+20 O(\log n) logarithmic order
2n+3 n\log_{2}n+19 O(n\log n) n\log norder
6n^{3}+ 2n^{2}+3n+4 O(n^{3}) cubic order
2^{n} O(2^{n}) Exponential order

The time spent on commonly used time complexity is from small to large in order:

O(1)<O(\log n)<O(n)<O(n\log n)<O(n^{2})<O(n^{3})<O(2^{n})<O(n!)<O(n^{n})

最坏情况运行时间时一种保证,那就是运行时间将不会再坏了。在应用中,注释一种最重要的需求,通常,除非特别指定,我们提到的运行时间都是最坏情况的运行时间。

平均运行时间也就是从概率的角度看,这个数字在每一个位置的可能性是相同的,所以平均的查找时间为n/2次后发现这个目标元素。

平均运行时间是所有情况中最有意义的,因为它是期望的运行时间。

一般在没有特殊说明的情况下,都是指最坏时间复杂度。 


 算法空间复杂度:通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。

一般情况下,一个程序在机器上执行时,除了需要存储程序本身的指令、常数、变量和输入数据外,还需要存储对数据操作的存储单元。若输入数据所占空间只取决于问题本身,和算法无关,这样只需要分析该算法在实现时所需的辅助单元即可。若算法执行时所需的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为O(1)。
通常,我们都使用“时间复杂度”来指运行时间的需求,使用“空间复杂度”指空间需求。当不用限定词地使用“复杂度”时,通常都是指时间复杂度。显然我们这本书重点要讲的还是算法的时间复杂度的问题。


 总结回顾

  • 算法的定义:算法是解决特定问题求解步骤的描述,在计算机中为指令的有限序列,并且每条指令表示一个或多个操作。
  • 算法的特性:有穷性、确定性、可行性、输入、输出。
  • 算法的设计的要求:正确性、可读性、健壮性、高效率和低存储量需求。
  • 算法的度量方法:事后统计方法(不科学、不准确)、事前分析估算方法。
  • 函数的渐近增长:给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n > N,f(n)总是比g(n)大,那么,我们说f(n)的增长渐近快于g(n)。
  • 某个算法,随着n的变大,它会越来越优于另一算法,或者越来越差于另一算法。
  • 推导大O阶:

■ 用常数1取代运行时间中的所有加法常数。
■ 在修改后的运行次数函数中,只保留最高阶项。
■ 如果最高阶项存在且不是1,则去除与这个项相乘的常数。
得到的结果就是大O阶。

  • 常用的时间复杂度所耗费的时间从小到大依次是:

    O(1)<O(\log n)<O(n)<O(n\log n)<O(n^{2})<O(n^{3})<O(2^{n})<O(n!)<O(n^{n})

第 3 章 线性表

线性表:零个或多个数据元素的有限序列。

  • 首先它是一个序列。也就是说,元素之间是有顺序的,若元素存在多个,则第一个元素无前驱,最后一个元素无后继,其他每个元素都有且只有一个前驱和后继。如果一个小朋友去拉两个小朋友后面的衣服,那就不可以排成一队了;同样,如果一个小朋友后面的衣服,被两个甚至多个小朋友拉扯,这其实是在打架,而不是有序排队。
  • 然后,线性表强调是有限的,小朋友班级人数是有限的,元素个数当然也是有限的。事实上,在计算机中处理的对象都是有限的,那种无限的数列,只存在于数学的概念中。
  • 如果用数学语言来进行定义。可如下:
  • 若将线性表记为(a1,…,ai-1,ai,ai+1,…,an),则表中ai-1领先于ai,ai领先于ai+1,称ai-1是ai的直接前驱元素,ai+1是ai的直接后继元素。当i=1,2,…,n-1时,ai有且仅有一个直接后继,当i=2,3,…,n时,ai有且仅有一个直接前驱。如图所示。
  •  所以线性表元素的个数n(n≥0)定义为线性表的长度,当n=0时,称为空表
  • 在非空表中的每个数据元素都有一个确定的位置,如a1是第一个数据元素,an是最后一个数据元素,ai是第i个数据元素,称i为数据元素ai在线性表中的位序
  • 在较复杂的线性表中,一个数据元素可以由若干个数据项组成。
  • 在较复杂的线性表中,一个数据元素可以由若干个数据项组成。
  • 用线性表的定义来说,要相同类型的数据

线性表的抽象数据类型定义如下:

  • 对于不同的应用,线性表的基本操作是不同的,上述操作是最基本的,对于实际问题中涉及的关于线性表的更复杂操作,完全可以用这些基本操作的组合来实现。
  • 比如,要实现两个线性表集合A和B的并集操作。即要使得集合A=A∪B。说白了,就是把存在集合B中但并不存在A中的数据元素插入到A中即可。
  • 假设La表示集合A,Lb表示集合B,则实现的代码如下:

 

 


线性表的顺序存储结构

顺序存储定义:用一段地址连续的存储单元依次存储线性表的数据元素。
线性表(a1,a2,……,an)的顺序存储示意图如下:

 顺序存储方式:既然线性表的每个数据元素的类型都相同,所以可以用C语言(其他语言也相同)的一维数组来实现顺序存储结构,即把第一个数据元素存到数组下标为0的位置中,接着把线性表相邻的元素存储在数组中相邻的位置。

随着数据的插入,我们线性表的长度开始变大,不过线性表的当前长度不能超过存储容量,即数组的长度。

线性表的顺序存储的结构代码。

    #define MAXSIZE 20          /*存储空间初始分配量*/
    typedef int ElemType;       /*ElemType类型根据实际情况而定,这里假设为int*/
    typedef struct
    {
        ElemType data[MAXSIZE];       /*数组存储数据元素,最大值为MAXSIZE*/
        int length;                   /*线性表当前长度*/
    }SqList;


这里,我们就发现描述顺序存储结构需要三个属性:
■ 存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。
■ 线性表的最大存储容量:数组长度MaxSize。
■ 线性表的当前长度:length。

 数据长度与线性表长度区别:数组的长度是存放线性表的存储空间的长度;线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的。
在任意时刻,线性表的长度应该小于等于数组的长度。

地址计算方法:数组从0开始第一个下标,线性表的第i个元素是要存储在数组下标为i-1的位置。

  •  用数组存储顺序表意味着要分配固定长度的数组空间,由于线性表中可以进行插入和删除操作,因此分配的数组空间要大于等于当前线性表的长度
  • 存储器中的每个存储单元都有自己的编号,这个编号称为地址。当我们占座后,占座的第一个位置确定后,后面的位置都是可以计算的

线性表中第i+1个数据元素的存储位置和第i个数据元素的存储位置满足下列关系(LOC表示获得存储位置的函数)。
LOC(a_{i+1})=LOC(ai)+c
所以对于第i个数据元素ai的存储位置可以由a1推算得出:
LOC(ai)=LOC(a1)+(i-1)*c

  • 对每个线性表位置的存入或者取出数据,对于计算机来说都是相等的时间,也就是一个常数,因此用我们算法中学到的时间复杂度的概念来说,它的存取时间性能为O(1)。我们通常把具有这一特点的存储结构称为随机存取结构。 

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

 获得元素操作

将线性表L中的第i个位置元素值返回就是把数组第i-1下标的值返回即可。来看代码:

    #define OK 1
    #define ERROR 0
    #define TRUE 1
    #define FALSE 0
    typedef int Status;
    /*Status是函数的类型,其值是函数结果状态代码,如OK等*/
    /*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
    /*操作结果:用e返回L中第i个数据元素的值*/
    Status GetElem(SqList L,int i,ElemType *e)
    {
         if(L.length==0 || i<1 || i>L.length)
             return ERROR;
        *e=L.data[i-1];
        return OK;
    }


注意这里返回值类型Status是一个整型,返回OK代表1,ERROR代表0。之后代码中出现就不再详述。

 插入操作:

即在线性表L中的第i个位置插入新元素e。

 插入算法的思路:
■ 如果插入位置不合理,抛出异常;
■ 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
■ 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
■ 将要插入元素填入位置i处;

■ 表长加1。
实现代码如下:

 删除操作

删除算法的思路:
■ 如果删除位置不合理,抛出异常;
■ 取出删除元素;
■ 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
■ 表长减1。

实现代码如下:

    /*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
    /*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1*/
    Status ListDelete(SqList *L,int i,ElemType *e)
    {
        int k;
        if (L->length==0)                /*线性表为空*/
           return ERROR;
        if (i<1 || i>L->length)          /*删除位置不正确*/
            return ERROR;
        *e=L->data[i-1];
        if (i<L->length)                 /*如果删除不是最后位置*/
        {
            for(k=i;k<L->length;k++)     /*将删除位置后继元素前移*/
               L->data[k-1]=L->data[k];
        }
        L->length--;
        return OK;
    }

插入和删除的时间复杂度

  • 先来看最好的情况,如果元素要插入到最后一个位置,或者删除最后一个元素,此时时间复杂度为O(1),因为不需要移动元素的,就如同来了一个新人要正常排队,当然是排在最后,如果此时他又不想排了,那么他一个人离开就好了,不影响任何人。
  • 最坏的情况呢,如果元素要插入到第一个位置或者删除第一个元素,此时时间复杂度是多少呢?那就意味着要移动所有的元素向后或者向前,所以这个时间复杂度为O(n)
  • 至于平均的情况,由于元素插入到第i个位置,或删除第i个元素,需要移动n-i个元素。根据概率原理,每个位置插入或删除元素的可能性是相同的,也就说位置靠前,移动元素多,位置靠后,移动元素少最终平均移动次数和最中间的那个元素的移动次数相等,为(n-1)/2。我们前面讨论过时间复杂度的推导,可以得出,平均时间复杂度还是O(n)
  • 这说明什么?线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是O(1);而插入或删除时,时间复杂度都是O(n)。这就说明,它比较适合元素个数不太变化,而更多是存取数据的应用。当然,它的优缺点还不只这些……
线性表的顺序存储结构的优缺点
优点 缺点
  • 无须为表示表中元素之间的逻辑关系而增加额外的存储空间
  • 可以快速地存取表中任一位置的元素
  • 插入和删除操作需要移动大量元素
  • 当线性表长度变化较大是,难以确认存储空间的容量
  • 造成存储空间的“碎片”

 


线性表的链式存储结构

线性表链式存储结构定义:线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。

  •  现在链式结构中,除了要存数据元素信息外,还要存储它的后继元素的存储地址
  • 因此,为了表示每个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域指针域中存储的信息称做指针或链。这两部分信息组成数据元素ai的存储映像,称为结点(Node)
  • n个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,…,an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起,如图所示。

  •  链表中第一个结点的存储位置叫做头指针,之后的每一个结点就是上一个后继指针指向的位置。
  • 线性链表的最后一个结点指针为“空”(通常用NULL或者"^"符号表示)
  • 在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息;也可以存储如线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针。

头指针与头结点的异同

 

 线性表链式存储结构代码描述

若线性表为空表,则头结点的指针域为“空”,如下图

 改用更方便的存储示意图来表示单链表,如下图

 若带有头结点的单链表,如下图

 空链表如下图

 结点由存放数据元素的数据域存放后继结点地址的指针域组成。假设p是指向线性表第i个元素的指针,则该结点ai的数据域我们可以用p->data来表示,p->data的值是一个数据元素,结点ai的指针域可以用p->next来表示,p->next的值是一个指针。p->next指向谁呢?当然是指向第i+1个元素,即指向ai+1的指针。也就是说,如果p->data=ai,那么p->next->data=ai+1,如下图


单链表读取

对于单链表实现获取第i个元素的数据的操作GetElem,在算法上,相对要麻烦一些。
获得链表第i个数据的算法思路:
1.声明一个结点p指向链表第一个结点,初始化j从1开始;
2.当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
3.若到链表末尾p为空,则说明第i个元素不存在;
4.否则查找成功,返回结点p的数据。
实现代码算法如下:

    /*初始条件:顺序线性表L已存在,1≤i≤ListLength(L)*/
    /*操作结果:用e返回L中第i个数据元素的值*/
    Status GetElem(LinkList L,int i,ElemType *e)
    {
        int j;
        LinkList p;          /*声明一结点p*/
        p = L->next;         /*让p指向链表L的第一个结点*/
        j = 1;               /*j为计数器*/
        while (p && j<i)   /*p不为空或者计数器j还没有等于i时,循环继续*/
        {
            p = p->next;     /*让p指向下一个结点*/
            ++j;
        }
        if ( !p || j>i )
            return ERROR;    /*第i个元素不存在*/
        *e = p->data;        /*取第i个元素的数据*/
        return OK;
    }


说白了,就是从头开始找,直到第i个元素为止。由于这个算法的时间复杂度取决于i的位置,当i=1时,则不需遍历,第一个就取出数据了,而当i=n时则遍历n-1次才可以。因此最坏情况的时间复杂度是O(n)。

由于单链表的结构中没有定义表长,所以不能事先知道要循环多少次,因此也就不方便使用for来控制循环。其主要核心思想就是“工作指针后移”,这其实也是很多算法的常用技术。


单链表的插入与删除(时间复杂度都是O(n))

单链表的插入

假设存储元素e的结点为s,要实现结点p、p->next和s之间逻辑关系的变化,只需将结点s插入到结点p和p->next之间即可。

根本用不着惊动其他结点,只需要让s->next和p->next的指针做一点改变即可。

  s->next=p->next; p->next=s;

解读这两句代码,也就是说让p的后继结点改成s的后继结点,再把结点s变成p的后继结点,如下图

单链表第i个数据插入结点的算法思路:
1.声明一结点p指向链表第一个结点,初始化j从1开始;
2.当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
3.若到链表末尾p为空,则说明第i个元素不存在;
4.否则查找成功,在系统中生成一个空结点s;
5.将数据元素e赋值给s->data;
6.单链表的插入标准语句s->next=p->next; p->next=s;
7.返回成功。

实现代码算法如下:

 /*初始条件:顺序线性表L已存在,1≤i≤ListLength(L),*/
    /*操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1*/
    Status ListInsert(LinkList *L,int i,ElemType e)
    {
        int j;
        LinkList p,s;
        p = *L;
        j = 1;
        while (p && j < i)     /* 寻找第i个结点 */
        {
            p = p->next;
            ++j;
        }
        if (!p || j > i)
            return ERROR;        /*第i个元素不存在*/
        s = (LinkList)malloc(sizeof(Node));/*生成新结点(C标准函数)*/
        s->data = e;
        s->next = p->next;      /*将p的后继结点赋值给s的后继*/
        p->next = s;            /*将s赋值给p的后继*/
        return OK;
    }

在这段算法代码中,我们用到了C语言的malloc标准函数,它的作用就是生成一个新的结点,其类型与Node是一样的,其实质就是在内存中找了一小块空地,准备用来存放e数据s结点。

 单链表的删除

将它的前继结点的指针绕过,指向它的后继结点即可,如下图

实际上就是一步,p->next=p->next->next,用q来取代p->next,即是

  q=p->next; p->next=q->next;

单链表第i个数据删除结点的算法思路:
1.声明一结点p指向链表第一个结点,初始化j从1开始;
2.当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
3.若到链表末尾p为空,则说明第i个元素不存在;
4.否则查找成功,将欲删除的结点p->next赋值给q;
5.单链表的删除标准语句p->next=q->next;
6.将q结点中的数据赋值给e,作为返回;
7.释放q结点;
8.返回成功。

实现代码算法如下:

    /*初始条件:顺序线性表L已存在,1≤i≤ListLength(L) */
    /*操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1*/
    Status ListDelete(LinkList *L, int i, ElemType *e)
    {
        int j;
        LinkList p, q;
        p = *L;
        j = 1;
        while (p->next && j < i)    /*遍历寻找第i个元素*/
        {
             p = p->next;
             ++j;
        }
        if (!(p->next) || j > i)
             return ERROR;        /*第i个元素不存在*/
        q = p->next;
        p->next = q->next;        /*将q的后继赋值给p的后继*/
        *e = q->data;             /*将q结点中的数据给e*/
        free(q);                /*让系统回收此结点,释放内存*/
        return OK;
    }

这段算法代码里,我们又用到了另一个C语言的标准函数free。它的作用就是让系统回收一个Node结点,释放内存。
分析一下刚才我们讲解的单链表插入和删除算法,我们发现,它们其实都是由两部分组成:第一部分就是遍历查找第i个元素;第二部分就是插入和删除元素。

单链表数据结构在插入和删除操作上,与线性表的顺序存储结构是没有太大优势的。但如果,我们希望从第i个位置,插入10个元素,对于顺序存储结构意味着,每一次插入都需要移动n-i个元素,每次都是O(n)。而单链表,我们只需要在第一次时,找到第i个位置的指针,此时为O(n),接下来只是简单地通过赋值移动指针而已,时间复杂度都是O(1)。显然,对于插入或删除数据越频繁的操作,单链表的效率优势就越是明显


单链表的整表创建

数组的初始化:声明一个类型和大小的数组并赋值的过程。

创建单链表的过程就是一个动态生成链表的过程。即从“空表”的初始状态起,依次建立各元素结点,并逐个插入链表。
单链表整表创建的算法思路:
1.声明一结点p和计数器变量i;
2.初始化一空链表L;
3.让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
4.循环:
◆ 生成一新结点赋值给p;
◆ 随机生成一数字赋值给p的数据域p->data;
◆ 将p插入到头结点与前一新结点之间。
实现代码算法如下:

/* 随机产生n个元素的值,建立带表头结点的单链线性表L(头插法)*/
    void CreateListHead(LinkList *L, int n)
    {
        LinkList p;
        int i;
        srand(time(0));                     /*初始化随机数种子*/
        *L = (LinkList)malloc(sizeof(Node));
        (*L)->next = NULL;                    /*先建立一个带头结点的单链表*/
        for (i=0; i<n; i++)
        {
            p = (LinkList)malloc(sizeof(Node));/*生成新结点*/
            p->data = rand()%100+1;          /*随机生成100以内的数字*/
            p->next = (*L)->next;
            (*L)->next = p;                  /*插入到表头*/
        }
    }

这段算法代码里,我们其实用的是插队的办法,就是始终让新结点在第一的位置。我也可以把这种算法简称为头插法,如下图

把每次新结点都插在终端结点的后面,这种算法称之为尾插法
实现代码算法如下:

   /* 随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法)*/
    void CreateListTail(LinkList *L, int n)
    {
        LinkList p,r;
        int i;
        srand(time(0));                       /*初始化随机数种子*/
        *L = (LinkList)malloc(sizeof(Node));/*为整个线性表*/
        r=*L;                                    /*r为指向尾部的结点*/
        for (i=0; i<n; i++)
        {
            p = (Node *)malloc(sizeof(Node)); /*生成新结点*/
            p->data = rand()%100+1;        /*随机生成100以内的数字*/
            r->next=p;                       /*将表尾终端结点的指针指向新结点*/
            r = p;                           /*将当前的新结点定义为表尾终端结点*/
        }
        r->next = NULL;                      /*表示当前链表结束*/
    }

注意L与r的关系,L是指整个单链表,而r是指向尾结点的变量,r会随着循环不断地变化结点,而L则是随着循环增长为一个多结点的链表。

r->next=p;的意思,其实就是将刚才的表尾终端结点r的指针指向新结点p,如下图所示,当中①位置的连线就是表示这个意思。

 它的意思,就是本来r是在ai-1元素的结点,可现在它已经不是最后的结点了,现在最后的结点是ai,所以应该要让将p结点这个最后的结点赋值给r。此时r又是最终的尾结点了。
循环结束后,那么应该让这个链表的指针域置空,因此有了“r->next=NULL;”,以便以后遍历时可以确认其是尾部。


单链表的整表删除

单链表整表删除的算法思路如下:
1.声明一结点p和q;
2.将第一个结点赋值给p;
3.循环:
◆ 将下一结点赋值给q;
◆ 释放p;
◆ 将q赋值给p。


单链表结构与顺序存储结构优缺点

 ■ 若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构。

■ 当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。而如果事先知道线性表的大致长度,比如一年12个月,一周就是星期一至星期日共七天,这种用顺序存储结构效率会高很多。


静态链表

静态链表:用数组描述的链表。

我们对数组第一个和最后一个元素作为特殊元素处理,不存数据。我们通常把未被使用的数组元素称为备用链表。而数组第一个元素,即下标为0的元素的cur就存放备用链表的第一个结点的下标;而数组的最后一个元素的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点作用,当整个链表为空时,则为0。如下图

 

 此时“甲”这里就存有下一元素“乙”的游标2,“乙”则存有下一元素“丁”的下标3。而“庚”是最后一个有值元素,所以它的cur设置为0。而最后一个元素的cur则因“甲”是第一有值元素而存有它的下标为1。而第一个元素则因空闲空间的第一个元素下标为7,所以它的cur存有7。

静态链表的插入操作

■ 当我们执行插入语句时,我们的目的是要在“乙”和“丁”之间插入“丙”。调用代码时,输入i值为3。
■ 第4行让k=MAX_SIZE–1=999。
■ 第7行,j=Malloc_SSL(L)=7。此时下标为0的cur也因为7要被占用而更改备用链表的值为8。
■ 第11~12行,for循环l由1到2,执行两次。代码k=L[k].cur; 使得k=999,得到k=L[999].cur=1,再得到k=L[1].cur=2。
■ 第13行,L[j].cur=L[k].cur;因j=7,而k=2得到L[7].cur=L[2].cur=3。这就是刚才我说的让“丙”把它的cur改为3的意思。
■ 第14行,L[k].cur=j;意思就是L[2].cur=7。也就是让“乙”得点好处,把它的cur改为指向“丙”的下标7。
就这样,我们实现了在数组中,实现不移动元素,却插入了数据的操作,如下图

 静态链表的删除操作

“甲”现在要走,这个位置就空出来了,也就是,未来如果有新人来,最优先考虑这里,所以原来的第一个空位分量,即下标是8的分量,它降级了,把8给“甲”所在下标为1的分量的cur,也就是space[1].cur=space[0].cur=8,而space[0].cur=k=1其实就是让这个删除的位置成为第一个优先空位,把它存入第一个元素的cur中

 静态链表优缺点


循环链表

将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个,这种头尾相接的单链表称为单循环链表,简称循环链表(circular linked list)。

 循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p->next是否为空,现在则是p -> next不等于头结点,则循环未结束。

改用指向终端结点的尾指针来表示循环链表,如下图,此时查找开始结点和终端结点都很方便了。

 从上图中可以看到,终端结点用尾指针rear指示,则查找终端结点是O(1),而开始结点,其实就是rear->next->next,其时间复杂也为O(1)。

合并之后

 


双向链表 

在单链表的每个结点中,再设置一个指向其前驱结点,在设置一个指向其前驱结点的指针域,一个指向直接后继,另一个指向直接前驱。

第4章 栈与队列

栈与队列:
栈是限定仅在表尾进行插入和删除操作的线性表。
队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。

栈(stack)是限定仅在表尾进行插入和删除操作的线性表。
把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构。

 理解栈的定义需要注意:
首先它是一个线性表,也就是说,栈元素具有线性关系,即前驱后继关系。只不过它是一种特殊的线性表而已。定义中说是在线性表的表尾进行插入和删除操作,这里表尾是指栈顶,而不是栈底。
它的特殊之处就在于限制了这个线性表的插入和删除位置,它始终只在栈顶进行。这也就使得:栈底是固定的,最先进栈的只能在栈底。
栈的插入操作,叫作进栈,也称压栈、入栈。类似子弹入弹夹。

栈的删除操作,叫作出栈,也称弹栈。


进栈出栈变化形式

栈对线性表的插入和删除的位置进行了限制,并没有对元素进出的时间进行限制,也就是说,在不是所有元素都进栈的情况下,事先进去的元素也可以出栈,只要保证是栈顶元素出栈就可以


栈的抽象数据类型

ADT 栈(stack)
    Data
        同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
    Operation
        InitStack(*S):初始化操作,建立一个空栈S。
        DestroyStack(*S):若栈存在,则销毁它。
        ClearStack(*S):将栈清空。
        StackEmpty(S):若栈为空,返回true,否则返回false。
        GetTop(S,*e):若栈存在且非空,用e返回S的栈顶元素。
        Push(*S,e):若栈S存在,插入新元素e到栈S中并成为栈顶元素。
        Pop(*S,*e):删除栈S中栈顶元素,并用e返回其值。
        StackLength(S):返回栈S的元素个数。
    endADT


栈的顺序存储结构及实现

栈的顺序存储是线性表顺序存储的简化,简称为顺序栈。

定义一个top变量来指示栈顶元素在数组中的位置

 栈的顺序存储结构——进栈操作

栈的插入,即进栈操作,做了如下图的处理

 栈的顺序存储结构——出栈操作


两栈共享空间

其实栈的顺序存储还是很方便的,因为它只准栈顶进出元素,所以不存在线性表插入和删除时需要移动元素的问题。不过它有一个很大的缺陷,就是必须事先确定数组存储空间大小,万一不够用了,就需要编程手段来扩展数组的容量,非常麻烦。

如果我们有两个相同类型的栈,我们为它们各自开辟了数组空间,极有可能是第一个栈已经满了,再进栈就溢出了,而另一个栈还有很多存储空间空闲。这又何必呢?我们完全可以用一个数组来存储两个栈,只不过需要点小技巧。
我们的做法如下图,数组有两个端点,两个栈有两个栈底,让一个栈的栈底为数组的始端,即下标为0处,另一个栈为栈的末端,即下标为数组长度n-1处。这样,两个栈如果增加元素,就是两端点向中间延伸。

其实关键思路是:它们是在数组的两端,向中间靠拢。top1和top2是栈1和栈2的栈顶指针,可以想象,只要它们俩不见面,两个栈就可以一直使用。
从这里也就可以分析出来,栈1为空时,就是top1等于-1时;而当top2等于n时,即是栈2为空时,那什么时候栈满呢?
想想极端的情况,若栈2是空栈,栈1的top1等于n-1时,就是栈1满了。反之,当栈1为空栈时,top2等于0时,为栈2满。但更多的情况,其实就是我刚才说的,两个栈见面之时,也就是两个指针之间相差1时,即top1+1==top2为栈满。


栈的链式存储结构及实现

栈的链式存储结构,简称为链栈。

通常对于链栈来说,是不需要头结点的。

 栈的链式存储结构——进栈操作

 假设元素值为e的新结点是s,top为栈顶指针,

/* 插入元素e为新的栈顶元素 */
    Status Push(LinkStack *S, SElemType e)
    {
        LinkStackPtr s=(LinkStackPtr)malloc(sizeof(StackNode));
        s->data=e;
        s->next=S->top;/* 把当前的栈顶元素赋值给新结点的直接后继,如图中① */
        S->top=s;      /* 将新的结点s赋值给栈顶指针,如图中② */
        S->count++;
        return OK;
    }

栈的链式存储结构——出栈操作

 假设变量p用来存储要删除的栈顶结点,将栈顶指针下移一位,最后释放p即可。

链栈的进栈push和出栈pop都很简单,没有任何循环操作,时间复杂度均为O(1)。

对比一下顺序栈与链栈,它们在时间复杂度上是一样的,均为O(1)。对于空间性能,顺序栈需要事先确定一个固定的长度,可能会存在内存空间浪费的问题,但它的优势是存取时定位很方便,而链栈则要求每个元素都有指针域,这同时也增加了一些内存开销,但对于栈的长度无限制。所以它们的区别和线性表中讨论的一样,如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈会更好一些。


队列的定义

队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。

队列插入数据只能在队尾进行,删除数据只能在队头进行。


循环队列

队列属性存储的不足

我们假设一个队列有n个元素,则顺序存储的队列需建立一个大于n的数组,并把队列的所有元素存储在数组的前n个单元,数组下标为0的一端即是队头。所谓的入队列操作,其实就是在队尾追加一个元素,不需要移动任何元素,因此时间复杂度为O(1),如图4-12-1所示。

图4-12-1
与栈不同的是,队列元素的出列是在队头,即下标为0的位置,那也就意味着,队列中的所有元素都得向前移动,以保证队列的队头,也就是下标为0的位置不为空,此时时间复杂度为O(n),如图4-12-2所示。

图4-12-2

 为了改善不足:对头不需要一定在下标为0的位置。

为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,所以引入两个指针,front指针指向队头元素,rear指针指向队尾元素的下一个位置,这样当front等于rear时,此队列不是还剩一个元素,而是空队列。 

 假设这个队列的总个数不超过5个,但目前如果接着入队的话,因数组末尾元素已经占用,再向后加,就会产生数组越界的错误,可实际上,我们的队列在下标为0和1的地方还是空闲的。我们把这种现象叫做“假溢出”。
现实当中,你上了公交车,发现前排有两个空座位,而后排所有座位都已经坐满,你会怎么做?立马下车,并对自己说,后面没座了,我等下一辆?
没有这么笨的人,前面有座位,当然也是可以坐的,除非坐满了,才会考虑下一辆。

循环队列定义(解决假溢出)

 把队列的这种头尾相接的顺序存储结构称为循环队列。

Guess you like

Origin blog.csdn.net/m0_62110645/article/details/131385851