栈的基本概念

  • 栈(Stack):一种操作受限的线性表,只允许在一端进行插入和删除操作的线性表。
    • 特点:后进先出(Last In First Out,LIFO)
    • 逻辑结构:与普通线性表相同
    • 数据的运算:插入、删除操作有区别
  • 重要术语:
    • 空栈:栈里面没有任何数据元素
    • 栈顶:允许插入和删除的一端
    • 栈底:不允许插入和删除的一端
  • 基本操作:
    • InitStack(&S)初始化栈。构造一个空栈 S,分配内存空间
    • DestroyStack(&S)销毁栈。销毁并释放栈 S 所占用的内存空间
    • Push(&S, x)进栈,若栈 S 未满,则将 x 加入使其成为新栈顶。
    • Pop(&S, &x)出栈,若栈 S 非空,则弹出栈顶元素,并用 x 返回。
    • GetTop(S, &x)读栈顶元素。若栈 S 非空,则用 x 返回栈顶元素。
    • StackEmpty(S):判断一个栈 S 是否为空。若 S 为空,返回 ture,否则返回 false。
  • n 个不同元素进栈,出栈元素不同排列的个数为$\frac{1}{n+1}C^n_{2n}$,上述公式称为卡特兰(Catalan)数,可采用数学归纳法证明。

顺序栈的实现

  • 顺序栈的定义

    1
    2
    3
    4
    5
    #define MaxSize 10	//定义栈中元素的最大个数
    typedef struct {
    ElemType date[MaxSize]; //静态数组存放栈中元素
    int top; //栈顶指针
    } SqStack;
  • 初始化操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #define MaxSize 10	//定义栈中元素的最大个数
    typedef struct {
    ElemType date[MaxSize]; //静态数组存放栈中元素
    int top; //栈顶指针
    } SqStack;

    //初始化栈
    void InitStack(Sqtack &S) {
    S.top = -1; //初始化栈顶指针
    }
    //判断栈空
    bool StackEmpty(SqStack S){
    if(S.top == -1) //栈空
    return true;
    else //不空
    return false;
    }
  • 进栈操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #define MaxSize 10	//定义栈中元素的最大个数
    typedef struct {
    ElemType date[MaxSize]; //静态数组存放栈中元素
    int top; //栈顶指针
    } SqStack;

    //新元素入栈
    bool Push(SqStack &S, ElemType x) {
    if(S.top == MaxSize - 1) //栈满,报错
    return false;
    S.top = S.top + 1; //指针先加1
    S.data[S.top] = x; //新元素入栈
    //以上两行代码等价于 S.data[++S.top] = x;
    return true;
    }
  • 出栈操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #define MaxSize 10	//定义栈中元素的最大个数
    typedef struct {
    ElemType date[MaxSize]; //静态数组存放栈中元素
    int top; //栈顶指针
    } SqStack;

    //出栈操作
    bool Pop(SqStack &S, ElemType &x) {
    if(S.top == -1) //栈空,报错
    return false;
    x = S.data[S.top]; //栈顶元素先出栈
    S.top = S.top - 1; //指针再减一
    //以上两行代码等价于 x = S.data[S.top--];
    return true;
    }
  • 读栈顶操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #define MaxSize 10	//定义栈中元素的最大个数
    typedef struct {
    ElemType date[MaxSize]; //静态数组存放栈中元素
    int top; //栈顶指针
    } SqStack;

    //出栈操作
    bool GetTop(SqStack &S, ElemType &x) {
    if(S.top == -1) //栈空,报错
    return false;
    x = S.data[S.top]; //x记录栈顶元素
    return true;
    }
  • 初始化,入栈,出栈和读栈顶都是O(1)时间复杂度。

  • SqStack S;声明栈时分配内存,函数运行结束后系统自动回收内存。

  • 栈满的条件:top == MaxSize - 1

  • 另一种方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #define MaxSize 10	//定义栈中元素的最大个数
    typedef struct {
    ElemType date[MaxSize]; //静态数组存放栈中元素
    int top; //栈顶指针
    } SqStack;

    //初始化栈
    void InitStack(Sqtack &S) {
    S.top = 0; //top指向下一个可以插入元素的位置
    }
    //判断栈空
    bool StackEmpty(SqStack S){
    if(S.top == 0) //栈空
    return true;
    else //不空
    return false;
    }
    //进栈
    S.data[S.top] = x;
    S.top = S.top + 1;
    等价于 S.data[S.top++] = x;
    //出栈
    S.top = S.top - 1;
    x = S.data[S.top];
    等价于 x = S.data[--S.top];

    栈满的条件:top == MaxSize

  • 顺序栈的缺点:栈的大小不可变。

  • 共享栈:两个栈共享同一片空间。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #define MaxSize 10	//定义栈中元素的最大个数
    typedef struct {
    ElemType date[MaxSize]; //静态数组存放栈中元素
    int top0; //0号栈栈顶指针
    int top1; //1号栈栈顶指针
    } SqStack;

    //初始化栈
    void InitStack(Sqtack &S) {
    S.top0 = -1; //初始化栈顶指针
    S.top1 = MaxSize;
    }

    栈满的条件:top0 + 1 == top1

链栈的实现

  • 链栈:用链式存储方式实现的栈。

  • 链栈的定义

    1
    2
    3
    4
    typedef struct Linknode {
    ElemType data; //数据域
    struct Linknode *next; //指针域
    } *LiStack; //栈类型定义
  • 进栈:对头结点后插操作。

  • 出栈:对头结点“后删”操作。

  • 进栈和出栈操作都只能在栈顶一端进行(链头作为栈顶)。

  • 实现方式推荐不带头结点。

队列的基本概念

  • 队列(Queue):一种操作受限的线性表,只允许在一端进行插入,在另一端删除的线性表。
    • 特点:先进先出(Firstt In First Out,FIFO)
  • 重要术语:
    • 空队列:队列里面没有任何数据元素。
    • 队尾:允许插入的一端。
    • 队头:允许删除的一端。
  • 基本操作:
    • InitQueue(&Q)初始化队列。构造一个空队列 Q。
    • DestroyQueue(&Q)销毁队列。销毁并释放队列 Q 所占用的内存空间
    • Push(&Q, x)进队,若队列 Q 未满,则将 x 加入使其成为新的队尾。
    • Pop(&Q, &x)出队,若队列 Q 非空,则弹出队顶元素,并用 x 返回。
    • GetTop(Q, &x)读队头元素。若队列 Q 非空,则用 x 返回队头元素。
    • QueuekEmpty(Q):判断一个队列 Q 是否为空。若 Q 为空,返回 ture,否则返回 false。

队列的顺序实现

  • 队列的顺序实现

    1
    2
    3
    4
    5
    #define MaxSize 10	//定义栈中元素的最大个数
    typedef struct {
    ElemeType date[MaxSize]; //静态数组存放队列元素
    int front, rear; //队头指针和队尾指针
    } SqQueue;

    队头指针指向头元素;队尾指针指向下一个应该插入的位置

  • 初始化操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #define MaxSize 10	//定义栈中元素的最大个数
    typedef struct {
    ElemeType date[MaxSize]; //静态数组存放队列元素
    int front, rear; //队头指针和队尾指针
    } SqQueue;

    //初始化队列
    void InitQueue(SqQueue &Q) {
    //初始时 队头、队尾指针指向0
    Q.rear = Q.front = 0;
    }
    //判断队列是否为空
    bool QueueEmpty(SqQueue) {
    if (Q.rear == Q.front)
    return true;
    else
    return false;
    }
  • 入队操作(循环队列)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #define MaxSize 10	//定义栈中元素的最大个数
    typedef struct {
    ElemeType date[MaxSize]; //静态数组存放队列元素
    int front, rear; //队头指针和队尾指针
    } SqQueue;

    //入队(循环队列)
    bool EnQueue(SqQueue &Q, ElemType x) {
    if ((Q.rear + 1) % MaxSize == Q.front)
    return false; //队满则报错
    Q.data[Q.rear] = x; //新元素插入队尾
    Q.rear = (Q.rear + 1) % MaxSize; //队尾指针加1取模
    return true;
    }
  • 出队操作(循环队列)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //出队(删除一个队头元素,并用x返回)
    bool DeQueue(SqQueue &Q, ElemType &x) {
    if (Q.rear == Q.front)
    return false; //队空则报错
    x = Q.data[Q.front];
    Q.front = (Q.front + 1) % MaxSize;
    return true;
    }
    //获取队头元素的值,用x返回
    bool GetHead(SqQueue Q, ElemType &x) {
    if (Q.rear == Q.front)
    return false; //队空则报错
    x = Q.data[Q.front];
    return true;
    }
  • 判断队列已满/已空

    • 方案一:

      • 队满条件:队尾指针的下一个位置是队头(Q.rear + 1) % MaxSize == Q.front
      • 队空条件:Q.rear == Q.front
      1
      2
      3
      4
      5
      #define MaxSize 10	//定义栈中元素的最大个数
      typedef struct {
      ElemeType date[MaxSize]; //静态数组存放队列元素
      int front, rear; //队头指针和队尾指针(初始化时 rear = front = 0)
      } SqQueue;
    • 方案二:

      • 队满条件:size == MaxSize
      • 队空条件:size == 0
      • 插入成功 size++;删除成功 size–
      1
      2
      3
      4
      5
      6
      #define MaxSize 10	//定义栈中元素的最大个数
      typedef struct {
      ElemeType date[MaxSize]; //静态数组存放队列元素
      int front, rear; //队头指针和队尾指针
      int size; //队列当前长度(初始化时:size = 0)
      } SqQueue;
    • 方案三:

      • 队满条件:front == rear && tag == 1
      • 队空条件:front == rear && tag == 0
      • 插入成功 tag = 0;删除成功 tag = 1
      1
      2
      3
      4
      5
      6
      #define MaxSize 10	//定义栈中元素的最大个数
      typedef struct {
      ElemeType date[MaxSize]; //静态数组存放队列元素
      int front, rear; //队头指针和队尾指针
      int tag; //最近进行的是删除/插入(初始化时:tag = 0)
      } SqQueue;
  • 注意:

    • 队尾指针指向队尾元素下一个位置
    • 队尾指针指向指向队尾元素
      • 初始化:front = 0;rear = n-1
      • 插入:队尾指针先后移,再插入元素
      • 判空:(Q.rear+1)%MaxSize == Q.front
      • 判满:
        • 牺牲一个存储单元
        • 增加辅助变量

队列的链式实现

  • 队列的链式实现

    1
    2
    3
    4
    5
    6
    7
    8
    typedef struct Linknode {	//链式队列结点
    ElemType data;
    struct Linknode *next;
    } LinkNode;

    typedef struct { //链式队列
    LinkNode *front, *rear; //队列的队头和队尾指针
    } LinkQueue;
  • 初始化(带头结点)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    typedef struct Linknode {	//链式队列结点
    ElemType data;
    struct Linknode *next;
    } LinkNode;

    typedef struct { //链式队列
    LinkNode *front, *rear; //队列的队头和队尾指针
    } LinkQueue;

    //初始化队列(带头结点)
    void InitQueue(LinkQueue &Q) {
    //初始时 front 和 rear 都指向头结点
    Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));
    Q.front->next = NULL;
    }
    //判断队列是否为空
    bool isEmpy(LinkQueue Q) {
    if (Q.front == Q.rear)
    return true;
    else
    return false;
    }

    初始化(不带头结点)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //初始化队列(不带头结点)
    void InitQueue(LinkQueue &Q) {
    //初始时 front 和 rear 都指向NULL
    Q.front = NULL;
    Q.rear= NULL;
    }
    //判断队列是否为空
    bool isEmpy(LinkQueue Q) {
    if (Q.front == NULL)
    return true;
    else
    return false;
    }
  • 入队(带头结点)

    1
    2
    3
    4
    5
    6
    7
    void EnQueue(LinkQueue &Q, ElemType x) {
    LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
    s->data = x;
    s->next = NULL;
    Q.rear->next = s; //新结点插入到rear之后
    Q.rear = s; //修改表尾指针
    }

    入队(不带头结点)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void EnQueue(LinkQueue &Q, ElemType x) {
    LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
    s->data = x;
    s->next = NULL;
    if (Q.front == NULL) { //在空队列中插入第一个元素
    Q.front = s; //修改队头队尾指针
    Q.rear = s;
    } else {
    Q.rear->next = s; //新结点插入到 rear 结点之后
    Q.rear = s; //修改 rear 指针
    }
  • 出队(带头结点)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //队头元素出队(带头结点)
    bool DeQueue(LinkQueue &Q, ElemType &x) {
    if(Q.front == Q.rear)
    return false; //空队
    LinkNode *p = Q.front->next;
    x = p->data; //用变量 x 返回队头元素
    Q.front->next = p->next; //修改头结点的 next 指针
    if(Q.rear == p) //此次是最后一个极点出队
    Q.rear == Q.front; //修改 rear 指针
    free(p); //释放结点空间
    return true;
    }

    出队(不带头结点)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //队头元素出队(不带头结点)
    bool DeQueue(LinkQueue &Q, ElemType &x) {
    if(Q.front == NULL)
    return false; //空队
    LinkNode *p = Q.front; //p 指向此次出队的结点
    x = p->data; //用变量 x 返回队头元素
    Q.front = p->next; //修改 front 指针
    if(Q.rear == p) //此次是最后一个极点出队
    Q.front == NULL; //front 指向 NULL
    Q.rear == NULL; //rear 指针 NULL
    free(p); //释放结点空间
    return true;
    }
  • 链式存储——一般不会队满,除非内存不足。

双端队列

  • 只允许从两端插入两端删除的线性表。
    • 输入受限的双端队列:只允许从一端插入两端删除的线性表。
    • 输出受限的双端队列:只允许从两端插入一端删除的线性表。

栈的应用——括号匹配问题

  • 最后出现的左括号最先被匹配(LIFO)

  • 算法实现:依次扫描所有字符,遇到左括号进栈,遇到右括号则弹出栈顶元素检查是否匹配。

    • 匹配失败情况:
      • 剩余左(右)括号
      • 左右括号不匹配
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    #define MaxSize 10	//定义栈中元素的最大个数(存在存满情况可用 链栈)
    typedef struct {
    char date[MaxSize]; //静态数组存放栈中元素
    int top; //栈顶指针
    } SqStack;

    //初始化栈
    void InitStack(SqStack &s);
    //判断栈是否为空
    bool StackEmpty(SqStack S);
    //新元素入栈
    bool Push(SqStack &S, char x);
    //栈顶元素出栈,用 x 返回
    bool Pop(SqStack &S, char &x);

    bool bracketCheck(char str[], int length) {
    SqStack S;
    InitStack(S); //初始化一个栈
    for (int i = 0; i < length; i++) {
    if (str[i] == '(' || str[i] == '[' || str[i] == '{') {
    Push(S, str[i]); //匹配到左括号,入栈
    } else {
    if(StackEmpty(S)) //匹配到右括号,且当前栈空
    return false; //匹配失效
    char topElem;
    Pop(S, topElem); //栈顶元素出栈
    if(str[i] == ')' && topElem!='(')
    return false;
    if (str[i] == ']' && topElem != '[')
    return false;
    if (str[i] == '}' && topElem != '{')
    return false;
    }
    }
    return StackEmpty(S);
    }

栈的应用——表达式求值

  • 中缀表达式:运算符在两个操作数中间
  • 后缀表达式(逆波兰表达式):运算符在两个操作数后面
  • 前缀表达式(波兰表达式):运算符在两个操作数前面
  • 中缀转后缀手算方法:
    1. 确定中缀表达式中各个运算符的运算顺序
    2. 选择下一个运算符,按照(左操作数 右操作数 运算符)的方式组成一个新的操作数
    3. 如果还有运算符没被处理,就继续上一步
  • “左优先“原则:只要左边的运算符能先计算,就优先算左边的。保证手算和机算结果相同。
  • 从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对运算,合体为一个操作数。
  • 用栈实现后缀表达式计算
    1. 从左往右扫描下一个元素,直到处理完所有元素
    2. 若扫描到操作数则压入栈,回到上一步;否则执行下一步
    3. 若扫描到运算符,则弹出两个栈顶元素(先出栈的是“右操作数”),执行相应运算,运算结果压回栈顶,回到第一步
  • 中缀转前缀手算方法:
    1. 确定中缀表达式中各个运算符的运算顺序
    2. 选择下一个运算符,按照(运算符 左操作数 右操作数)的方式组成一个新的操作数
    3. 如果还有运算符没被处理,就继续上一步
  • “右优先“原则:只要右边的运算符能先计算,就优先算右边的。保证手算和机算结果相同。
  • 用栈实现前缀表达式计算
    1. 从右往左扫描下一个元素,直到处理完所有元素
    2. 若扫描到操作数则压入栈,回到上一步;否则执行下一步
    3. 若扫描到运算符,则弹出两个栈顶元素(先出栈的是“左操作数”),执行相应运算,运算结果压回栈顶,回到第一步
  • 中缀表达式转后缀表达式(机算
    1. 初始化一个栈,用于保存暂时还不能确定运算顺序的运算符
    2. 从左到右处理各个元素,直到末尾。可能遇到三种情况:
      • 遇到操作数。直接假如后缀表达式。
      • 遇到界限符。遇到 “(“ 直接入栈;遇到 “)” 则依次弹出栈内运算符并加入后缀表达式,直到弹出 “(“ 为止。注意:”(“ 不加入后缀表达式。
      • 遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到 “(“ 或栈空则停止。之后再把当前运算符入栈。
    3. 按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。
  • 中缀表达式的计算(用栈实现):
    1. 初始化两个栈,操作数栈和运算符栈
    2. 若扫描到操作数,压入操作数栈
    3. 若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应元素,运算结果再压回操作数栈)

栈的应用——递归

  • 函数调用的特点:最后被调用的函数最先执行结束(LIFO)
  • 函数调用时,需要用到一个栈存储:
    • 调用返回地址
    • 实参
    • 局部变量
  • 适合用“递归”算法解决:可以把原始问题转换为属性相同,但规模较小的问题。例如:
    • 计算正整数的阶乘!
    • 求斐波那契数列
  • 递归调用时,函数调用栈可称为“递归工作栈”。
    • 每进入一层递归,就将递归调用所需信息压入栈顶。
    • 每退出一层递归,每退出一层递归,就从栈顶弹出相应信息。
  • 缺点:效率低,太多层递归可能会导致栈溢出;可能包含很多重复计算。
  • 可以自定义栈将递归算法改造成非递归算法。

队列的应用

  • 树的层次遍历
  • 图的广度优先遍历
  • 队列在操作系统中应用
    • 多个进程争抢着使用有限的系统资源,FCFS(First Come First Service,先来先服务)是一种常用策略。
    • 打印数据缓冲区。

特殊矩阵压缩存储

数组的存储结构

  • 一维数组的存储结构
    • ElemType a[10];
    • 起始地址:LOC
    • 各组元素大小相同,且物理上连续存放。
      • 数组元素 a[i] 的存放地址 = LOC+ i*sizeof(Elemtype)
      • 数组下标默认从0开始。
  • 二维数组的存储结构
    • 两种存储策略:行优先存储;列优先存储。
    • 起始地址:LOC
    • 在M行N列的二维数组中
      • 若行优先存储,b[ i ] [ j ]的存储地址 = LOC + (i * N + j) * sizeof(ElemType)
      • 若列优先存储,b[ i ] [ j ]的存储地址 = LOC + (i * M + i) * sizeof(ElemType)

普通矩阵的存储

  • 可用二维数组存储
  • 描述矩阵元素时行和列通常从1开始,描述数组时通常从0开始。

对称矩阵的压缩存储

  • 对称矩阵:元素值关于主对角线对称。
  • 策略:只存储主对角线 + 下三角区。
  • 按行优先原则将各元素存入一维数组中。
    • 共有$\frac{n * (n+1)}{2}$个元素,按行优先原则ai,j是第$\frac{i * (i -1)}{2} + j$个元素。
    • 找上三角区元素:找aj,i
  • 按列优先原则将各元素存入一维数组中。
    • 共有$\frac{n * (n+1)}{2}$个元素,按行优先原则ai,j是第[n + (n-1) + … + (n-j+2)] + (i-j) +1个元素。
    • 找上三角区元素:找aj,i

三角矩阵的压缩存储

  • 下三角矩阵:除了主对角线和下三角区,其余的元素都相同

  • 压缩存储策略:

    • 按行优先原则将下三角区存入一维数组中。并在最后一个位置存储常量C。

    • 共有$\frac{n * (n+1)}{2} + 1$个元素,按行优先原则:

      • 若i >= j,ai,j是第$\frac{i * (i -1)}{2} + j$个元素。
      • 若i < j,ai,j是第$\frac{n * (n + 1)}{2} + j$个元素。
    • 按行优先原则将上三角区存入一维数组中。并在最后一个位置存储常量C。

    • 共有$\frac{n * (n+1)}{2} + 1$个元素,按行优先原则:

      • 若i <= j,ai,j是第[n + (n - 1) + … + (n - j + 2)] + (i - j) +1个元素。
      • 若i > j,ai,j是第$\frac{n * (n + 1)}{2} + j$个元素。

三对角矩阵的压缩存储

  • 三对角矩阵:又称带状矩阵。主对角线及其相邻对角线元素可以不为0,其余元素均为0。(除了第一行和最后一行至多有两个不为0元素,其余行至多有三个不为0元素。
  • 共有3n-2个元素,按行优先原则,ai,j是第2*i+j-2个元素。
  • 第 k 个元素在第(k+2) /3行,第k-2*i+3列。

稀疏矩阵的压缩存储

  • 稀疏矩阵:非零元素远远少于矩阵元素的个数。
  • 策略:
    • 顺序存储——三元组<行,列,值>(struct
    • 链式存储——十字链表法
      • 向下域down:指向同列的下一个非零元素
      • 向右域right:指向同行的下一个非零元素

第一章 计算机系统漫游

  • 计算机系统是由硬件和系统软件组成的,他们共同工作来运行应用程序。

信息就是位+上下文

  • 源程序实际上就是一个由值0和1组成的为(又称为比特)序列,8个位组成一组,称为字节
  • 大部分的现代计算机系统都使用 ASCII 标准来表示文本字符,这种方式实际上就是用一个唯一的单字节大小的整数值来表示每个字符。
  • 只由 ASCII 字符构成的文件称为文本文件,所有其他文件称为二进制文件
  • 区别不同数据对象的唯一方法是我们读到这些数据对象时的上下文。

程序被其他程序翻译成不同的格式

  • 编译系统
    • 预处理阶段。预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。通常生成以.i为后缀名的文件。
    • 编译阶段。编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。
      • 汇编语言为不同高级语言的不同编译器提供了通用的输出语言。
    • 汇编阶段。汇编器(as)将hello.s翻译成及其语言指令。
    • 链接阶段。链接器将关于涉及到的函数的预编译文件整合到目标文件中,得到hello文件(可执行文件)。

了解编译系统如何工作是大有益处的

  • 优化程序性能
  • 理解链接时出现的错误
  • 避免安全漏洞

处理器读并解释储存在内存中的指令

  • shell是一个命令行解释器,它输出一个提示符,等待输入一个命令行,然后执行这个命令。

  • 系统的硬件组成

    • 总线:贯穿整个系统的是一组电子管道,它携带信息字节并负责在各个部件间传递。通常总线被设计成传送字长的字节块,也就是。字中的字节数(即字长)是一个基本的系统参数,各个系统中都不尽相同。现在的大多数机器字长要么是4个字节(32位),要么是8个字节(64位)。

    • I/O设备是系统和外部世界的联系通道。每个I/O设备通过一个控制器或适配器与I/O总线相连。

      • 控制器和适配器之间的区别主要在于它们的封装方式。
      • 控制器是I/O设备本身或者系统的主印制电路板(通常称作主板上)的芯片组。
      • 适配器是一块插在主板插槽上的卡。
    • 主存是一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。

      • 从物理上来说,主存是由一组动态随机存储器(DRAM)芯片组成的。
      • 从逻辑上来说,存储器是一个线性的字节数组,每个字节都有其唯一的地址(数组索引),这些地址是从零开始的。
    • 处理器是中央处理单元(CPU)的简称,是解释(或执行)存储在主存中指令的引擎。处理器的核心是一个大小为一个字的存储设备(或寄存器),称为程序计数器(PC)

      • 在任何时刻,PC都指向主存中的某条及其语言指令(即含有该条指令的地址)。而PC更新时指向的下一条指令并不一定和在内存中刚刚执行的指令相邻。

        • 加载:从主存复制一个字节或者一个字到寄存器,以覆盖寄存器原来的内容。
        • 存储:从寄存器复制一个字节或者一个字到主存的某个位置,以覆盖这个位置上原来的内容。
        • 操作:把两个寄存器的内容复制到ALU,ALU对这两个字做算术运算,并将结果存放到一个寄存器中,以覆盖该寄存器中原来的内容。
        • 跳转:从指令本身中抽取一个字,并将这个字复制到程序计数器(PC)中,以覆盖PC中原来的值。
      • 指令集架构描述的是每条机器代码指令的效果;而微体系结构描述的是处理器实际上是如何实现的。

  • 运行 hello 程序

    • 初始时,shell程序执行它的指令,等待我们输入一个指令。当我们在键盘上输入字符串./hello后,shell程序将字符逐一读入寄存器,再把它存放内存中。
    • 当我们在键盘上敲回车键时,shell程序就知道我们已经结束了命令的输入。然后shell执行一系列指令来加载可执行的hello文件。这些指令将hello目标文件中的代码和数据从磁盘复制到主存。数据包括最终会被输出的字符串"hello, world\n"
      • 利用直接存储器存取(DMA)技术,数据可以不通过处理器而直接从磁盘到达主存。
    • 一旦目标位文件中的hello中的代码和数据被加载到主存,处理器就开始执行hello程序的main程序中的机器语言指令。这些指令将"hello, world\n"字符串中的字节从主存赋值到寄存器文件,再从寄存器文件复制到显示设备,最终显示在屏幕上。

高速缓存至关重要

  • 较大的存储设备要比较小的存储设备运行得慢,而快速设备的造价远高于同类的低速设备。
  • 针对这种处理器和主存之间的差异,系统设计者采用了更小更快的存储设备,称为高速缓存存储器(cache memory,简称为cache或高速缓存),作为暂时的集结区域,存放处理器近期可能会需要的信息。

存储设备形成层次结构

  • 每个计算机系统中的存储设备都被组织成了一个存储器层次结构。
  • 存储器层次结构的主要思想是上一层的存储器作为低一层存储器的高速缓存。
  • 操作系统的两个基本功能:
    • 防止硬件被失控的应用程序滥用。
    • 向应用程序提供简单一致的机制来控制复杂而又通常大小不相同的低级硬件设备。
  • 文件是对I/O设备的抽象。
    • 文件就是字节序列,每个 I/O 设备,包括磁盘、键盘、显示器,甚至网络,都可以看成是文件。
  • 虚拟内存是对主存和磁盘 I/O 设备的抽象表示。
    • 每个进程看到的内存都是一致的,称为虚拟地址空间。它们可分为以下区域:
      • 程序代码和数据。对所有的进程来说,代码是从同一固定地址开始,紧接着的是和C全局变量相对应的数据位置。代码和数据区是直接按照可执行目标文件的内容初始化的。
      • 。代码和数据区后紧随着的是运行时堆。代码和数据区在进程的一开始运行时就被指定了大小,而堆可以在运行时动态地扩展和收缩。
      • 共享库。大约在地址空间的中间部分是一块用来存放像C标准库和数学库这样的共享库的代码和数据的区域。
      • 。位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。
      • 内核虚拟内存。地址空间顶部的区域是为内核保留的。不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。相反,它们必须调用内核来执行这些操作。
  • 进程是对处理器、主存和 I/O 设备的抽象表示。
    • 进程是操作系统对一个正在运行的程序的一种抽象。
    • 并发运行是说一个进程的指令和另一个进程的指令是交错执行的。
    • 传统系统在一个时刻只能执行一个程序,而先进的多核处理器同时能执行多个程序。
    • 无论是单核还是多核系统中,一个 CPU 看上去都像是在并发地执行多个进程,这是通过处理器在进程间切换来实现的。操作系统实现这种交错执行的机制称为上下文切换
    • 操作系统保持跟踪进程运行所需的所有状态信息。这种状态,也就是上下文,包括许多信息,比如PC和寄存器文件的当前值,以及主存的内容。当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,然后将控制权传递到新进程。
    • 从一个进程到另一个进程的转换是由操作系统内核管理的。内核不是一个独立的进程。相反,它是系统管理全部进程所用代码和数据结构的集合。
    • 一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。

系统之间利用网络通信

  • 现代系统经常通过网络和其他系统连接到一起。

重要主题

  • Amdahl定律的主要思想是当我们对系统的某个部分加速时,其对系统整体性能的影响取决于该部分的重要性和加速程度。
  • 并发是一个通用的概念,指一个同时具有多个活动的系统。
  • 并行指的是用并发来使一个系统运行得更快。
  • 处理必须在多个任务间切换,大多数实际的计算也都是由一个处理器来完成的。这种配置称为单处理系统
  • 当构建一个由但操作系统内核控制的多处理器组成的系统时,我们就得到了一个多处理系统
  • 多核处理器是将多个 CPU(称为“核”)集成在一个集成电路芯片上。
  • 超线程,有时称为多线程,是一项允许一个 CPU 执行多个控制流的技术。
  • 多处理器的使用可以从两方面提高系统性能。首先,它减少了在执行多个任务时模拟并发的需要。其次,它可以使应用程序执行地更快。
  • 在较低的抽象层次上,现代处理器可以同时执行多条指令的属性称为指令级并行
  • 流水线中,将执行一条指令所需要的活动划分成不同的步骤,将处理器的硬件组织成一系列的阶段,每个阶段执行一个步骤。
  • 如果处理器可以达到比一个周期一条指令更快的执行速率,就称之为超标量
  • 允许一条指令产生多个可以并行执行的操作,这种方式称为单指令、多数据,即SIMD并行。
  • 虚拟机提供对整个计算机的抽象,包括操作系统、处理器和程序。

第二章 信息的表示与处理

  • 现代计算机存储和处理的信息以二进制信号表示(),他们形成了数字革命的基础。
  • 当把位组合在一起,再加上某种解释,我们就可以描述任何有限集合的元素。
  • 无符号编码基于传统的二进制表示法,表示大于或等于零的数字。
  • 补码编码是表示有符号整数的最常见的方式,有符号整数就是可以为正或者为负的数字。
  • 浮点数编码是表示实数的科学记数法以2为基数的版本。
  • 当结果太大以至于不能表示时,某些运算就会溢出
  • 整数的表示虽然只能编码一个相对较小的数值范围,但是这种表示是精准的;而浮点数虽然可以编码一个较大的数值范围,但这种表示只是近似的。浮点数的计算可能因为运算过程的不同而影响正常结果,整数则不会。

信息存储

  • 大多数计算机使用8位的块(字节 byte),作为最小的可寻址的内存单位,而不是位。
  • 机器级程序将内存视为一个非常大的字节数组,称为虚拟内存
  • 内存的每一个字节都由一个唯一的数字来标识,称为它的地址
  • 所有可能的地址的集合就称为虚拟地址空间

十六进制表示法

  • 一个字节由8位组成。在二进制表示法中,它的值域是00000000~ 111111112。如果看成十进制整数,它的值域就是010 ~ 25510
  • 二进制和十六进制的之间的转换可以通过展开每个十六进制数字,将它转换为二进制格式。
  • 将一个十进制数字 x 转换为十六进制,可以反复地用16除 x。

字数据大小

  • 每台计算机都有一个字长(word size),指明指针数据的标称大小(nominal size)。字长决定的最重要的系统参数就是虚拟地址空间的最大大小。
  • 对于一个字长为 ω 位的机器而言,虚拟地址的范围为0 ~ 2ω  - 1。
  • 大多数64位机器也可以运行为32位机器编译的程序,这是一种向后兼容。
  • 数据类型 long 一般在32位程序中为4字节,在64位程序中则为8字节。为了避免由于依赖“典型”大小和不同编译器设置带来的奇怪行为,ISO C99引入一类数据类型,其数据大小是固定的,不随编译器和机器设置而变化,其中就有数据类型 int32_t 和 int64_t,它们分别为4个字节和8个字节。

寻址和字节顺序

  • 小端法:最低有效字节在最前面的方式。
  • 大端法:最高有效字节在最前面的方式。
  • 许多比较新的微处理器是双端法,可以把配置成作为大端或者小端的机器运行。
  • 反汇编器:一种确定可执行程序文件所表示的指令序列的工具。

表示字符串

  • 在使用 ASCII 码作为字符码的任何系统上都将得到相同的结果,与字节顺序和字大小无关。因而,文本数据比二进制数据具有更强的平台独立性。

表示代码

  • 不同的机器类型使用不同且不兼容的指令和编码方式。

布尔代数简介

  • 位向量就是固定长度为 ω 、由0和1组成的串。

  • ~ & 0 1 | 0 1 ^ 0 1
    0 1 0 0 0 0 0 1 0 0 1
    1 0 1 0 1 1 1 1 1 1 0

C 语言的位级运算

  • C 语言的一个有用的特性就是它支持布尔运算。
  • 位级运算的一个常见用法就是实现掩码运算,这里掩码是一个位模式,表示从一个字中选出的位的集合。

C语言中的逻辑计算

  • C语言还提供了一组逻辑运算符 || 、&& 和 !,分别对应 OR 、AND 和 NOT运算。逻辑运算认为所有非零的参数都表示 TRUE,而参数0表示 FALSE。它们返回1或者0,分别表示结果为 TRUE 或者为 FALSE。
  • 逻辑运算符 && 和 || 与它们对应的位级运算 & 和 | 之间第二个重要的区别是,如果对第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值。

C语言中的移位运算

  • C 语言还提供了一组移位运算,向左或者向右移动位模式。C 表达式 x<<k 会生成一个值,x 向左移动 k 位,丢弃最高的 k 位,并在右端补 k 个0。移位运算时从左至右可结合的。
  • 一般而言,机器支持两种形式的右移:逻辑右移算术右移
    • 逻辑右移是在左端补 k 个0。
    • 算术右移是在左端补 k 个最高有效位的值,它对有符号整数数据的运算非常有用。
  • C 语言标准并没有明确定义对于有符号数应该使用哪种类型的右移——算术右移或者逻辑右移都可以。
  • 对于无符号数,右移必须是逻辑的。
  • 与 C 相比,Java 对于如何进行右移有明确的定义。表达是 x>>k 会将 x 算术右移 k 个位置,而 x>>>k 会对 x 做逻辑右移。

整数表示

  • 人唯有把自己逼进万劫不复的险境,才能获得向死而生的勇气。
  • 人得以不断发展的重要动力是灰暗的生活与光明的未来之间的矛盾。
  • 勇敢的人先享受世界。
  • 人生的分界线就在这里,跨过这一步就是英雄,退回这一步就是懦夫。

  • 睿智,得乎静;刚勇,显与动。是故大丈夫处世,必忧思以深远,行决以神速,孤心磨砺以求成也。
  • 万事万物各行其道我又何必庸人自扰。
  • 去寻找全局最优解,而不是局部最优解。
  • 伟人之所以伟大,只是因为我们跪在地上,站起来吧!
  • 先不不喜欢的事情做好才能去做喜欢的事。
  • 故天将降大任于是人也,必先苦其心志,劳其筋骨,饿其体肤,空乏其身,行拂乱其所为,所以动心忍性,曾益其所不能。
  • 熬过每一个今天,成功自然会来。
  • 我们能够掌控的是能力,我们无法掌控的是机遇。没有机遇你展现不了能力,没有能力你抓不住机遇。我们能做的只有提升能力,抓住机遇。
  • 成功或失败是上天决定的,前进或停下是我决定的。命定但不注定。
  • 从无所不能,到无所能,到有所能。循此苦旅,以达天际。

  • 如果用一句话形容书我想是“润物细无声”。

  • 读书,是为了那些不能读书的人。

  • 自律就像是用气球包住了无数的欲望。这个气球会越来越大越来越大,随时都无比接近爆炸。但是只要自律还在,气球就不会爆炸。
  • 愈是沉迷什么,就愈要远离什么。你只能臣服于一种东西,那就是对自己强大的控制力。

把时间放到更有意义的事情上,是一种延长生命的行为。

线性表

线性表的定义

  • 线性表示具有相同数据类型的 n(n >= 0)个数据元素有限序列,其中 n 为表长,当 n = 0时线性表是一个空表。若用L命名线性表,则其一般表示为 L = (a1, a2, ai, ai+1, an)。每个数据元素所占空间一样大。
    • 有次序。
    • 有限(所有整数按递增次序排列不是一个线性表)。
    • ai 是线性表中的“第 i 个”元素线性表的位序
    • a1表头元素;an表尾元素
    • 除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继

线性表的基本操作

  • InitList(&L)初始化表。构造一个空的线性表 L,分配内存空间
  • Destroy(&L)销毁操作。销毁线性表,并释放线性表 L 所占用的内存空间
  • LinstInsert(&L, i, e)插入操作。在表L中的第 i 个位置上插入指定元素e。
  • ListDelete(&L, i, &e):删除操作。删除表 L 中的第 i 个位置的元素,并用 e 返回删除元素的值。
  • LocateElem(L, i)按值查找操作。在表 L 中查找具有给定关键字值的元素。
  • GetElem(L, i)按位查找操作。获取表 L 中第 i 个位置的元素的值。
  • Length(L):求表长。返回线性表 L 的长度,即 L 中数据元素的个数。
  • PrintList(L):输出操作。按前后顺序输出线性表 L 的所有元素值。
  • Empty(L):判空操作。若 L 为空表,则返回 true,否则返回 false。

顺序表

顺序表的定义

  • 用顺序存储的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。

顺序表的实现

  • 静态分配

    1
    2
    3
    4
    5
    #define MaxSize 10
    typedef struct {
    ElemType data[MaxSize];
    int length;
    }SqList;
  • 动态分配

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #define InitSize 10
    typedef struct {
    ElemType* data;
    int MaxSize;
    int lengeh;
    }SeqList;

    //增加动态数组的长度
    void IncreaseSize(SeqList& L, int len) {
    int* p = L.data;
    L.data = (int*)malloc((L.MaxSize + len) * sizeof(int));
    for(int i = 0; i < L.length; i++) {
    L.data[i] = p[i];
    }
    L.MaxSize = L.MaxSize + len;
    free(p);
    }
  • 顺序表的特点

    • 随机访问,即可以在 O(1) 时间内找到第i个元素。
    • 存储密度高,每个结点只存储数据元素。
    • 拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)。
    • 插入、删除操作不方便,需要移动大量元素。

顺序表的插入和删除

  • 插入操作。在表 L 中的第 i 个位置上插入指定元素e。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    bool LinstInsert(SqList& L, int i, int e) {
    if(i < 1 || i > L.length + 1)
    return false;
    if(L.length >= MaxSize)
    return false;
    for(int j = L.length; j >= i; j--) {
    L.data[j] = L.data[j - 1];
    }
    L.data[i - 1] = e;
    L.length++;
    return true;
    }

    最好时间复杂度:O(1)

    最坏时间复杂度:O(n)

    平均时间复杂度:O(n)

  • 删除操作。删除表 L 第 i 个位置的元素,并用 e 返回删除元素的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    bool ListDelete(SqList& L, int i, int& e) {
    if(i < 1 || i > L.length)
    return false;
    e = L.data[i - 1];
    for(int j = i; j < L.length; j++) {
    L.data[j - 1] = L.data[j];
    L.length--;
    return true;
    }
    }

    最好时间复杂度:O(1)

    最坏时间复杂度:O(n)

    平均时间复杂度:O(n)

顺序表的查找

  • 按位查找操作。获取表 L 中第 i 个位置的元素的值。

    1
    2
    3
    ElemType GetElem(SqList L, int i) {
    return L.data[i - 1];
    }

    时间复杂度:O(1)

  • 按值查找操作。在表 L 中查找具有给定关键字值的元素。

    1
    2
    3
    4
    5
    6
    int LocateElem(SeqList L, ElemType e) {
    for(int i = 0; i < L.length; i++)
    if(L.data[i] == e)
    return i + 1;
    return 0;
    }

    最好时间复杂度:O(1)

    最坏时间复杂度:O(n)

    平均时间复杂度:O(n)

单链表

单链表的定义

  • 顺序表:每个结点中只存放数据元素。

    • 优点:可随机存取,存储密度高。
    • 缺点:要求大片连续空间,改变容量不方便。
  • 单链表:每个结点除了存放数据元素外,还要存储指向下一个结点的指针。

    • 优点:不要求大片连续空间,改变容量方便。
    • 缺点:不可随机存取,要耗费一定空间存放指针。
  • 结构体定义

    1
    2
    3
    4
    typedef struct LNode {
    ElemType data;
    struct LNode* next;
    }LNode, * LinkList;

    强调这是一个单链表:使用LinkList

    强调这是一个结点:使用LNode*

  • 不带头结点的单链表

    1
    2
    3
    4
    bool InitList(LinkList& L) {
    L = NULL;// 空表,暂时还没有任何结点(防止脏数据)
    return true;
    }
  • 带头结点的单链表

    1
    2
    3
    4
    5
    6
    7
    bool InitList(LinkList& L) {
    L = (LNode*)malloc(sizeof(LNode));
    if(L == NULL)
    return false;
    L->next = NULL;
    return true;
    }

    头结点不存储数据。

单链表的插入

  • 插入操作。在表L中的第i个位置上插入指定元素e。

    • 带头结点
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    bool ListInsert(LinkList& L, int i, ElemType e) {
    if(i < 1)
    return false;
    LNode* p;
    int j = 0;
    p = L;
    while(p != NULL && j < i-1) {
    p = p->next;
    j++;
    }
    if(p == NULL)
    return false;
    LNode* s = (LNode*)malloc(sizeof(LNode));
    s->data = e;
    s->next = p->next;//此句和下一句不能颠倒
    p->next = s;
    return true;
    }

    最好时间复杂度:O(1)

    最坏时间复杂度:O(n)

    平均时间复杂度:O(n)

    • 不带头结点
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    bool ListInsert(LinkList& L, int i, ElemType e) {
    if(i < 1)
    return false;
    if(i == 1) {
    LNode* s = (LNode*)malloc(sizeof(LNode));
    s->data = e;
    s->next = L;
    L = s;
    return true;
    }
    LNode* p;
    int j = 1;
    p = L;
    while(p != NULL && j < i-1) {
    p = p->next;
    j++;
    }
    if(p == NULL)
    return false;
    LNode* s = (LNode*)malloc(sizeof(LNode));
    s->data = e;
    s->next = p->next;
    p->next = s;
    return true;
    }
  • 后插操作。在 p 结点之后插入元素。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    bool InsertNextNode(LNode* p, ElemType e) {
    if(p == NULL)
    return false;
    LNode* s = (LNode*)malloc(sizeof(LNode));
    if(s == NULL)// 某些情况下内存分配失败(如内存不足)
    return false;
    s->data = e;
    s->next = p->next;
    p->next = s;
    return true;
    }
  • 前插操作。在 p 结点之前插入元素 e。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    bool InsertPriorNode(LNode* p, ElemType e) {
    if(p == NULL)
    return false;
    LNode* s = (LNode*)malloc(sizeof(LNode));
    if(s == NULL)
    return false;
    s->next = p->next;
    p->next = s;//新节点s连到p之后
    s->data = p->data;//将p中元素复制到s中
    p->data = e;//p中元素覆盖为e
    return true;
    }

    时间复杂度:O(1)

单链表的删除

  • 删除操作。删除表中第 i 个位置的元素,并用 e 返回删除元素的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    bool ListDelete(LinkList& L, int i, ElemType& e) {
    if(i < 1)
    return false;
    LNode* p;
    int j = 0;
    p = L;
    while(p != NULL && j < i-1) {
    p = p->next;
    j++;
    }
    if(p == NULL)
    return false;
    LNode* q = p->next;
    e = q->data;
    p->next = q->next;
    free(q);
    return true;
    }

    最好时间复杂度:O(1)

    最坏时间复杂度:O(n)

    平均时间复杂度:O(n)

  • 删除指定结点p

    1
    2
    3
    4
    5
    6
    7
    8
    9
    bool DeleteNode(LNode* p) {
    if(p == NULL)
    return false;
    LNode* q = p->next;
    p->data = p->next->data;
    p->next = q->next;
    free(q);
    return true;
    }

    注意:不满足p是最后一个结点的情况。若需要删除最后一个结点,只能从表头依次寻找p的前驱。

单链表的查找

  • 按位查找,返回第i个元素(带头结点)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    LNode* GetElem(LinkList L, int i) {
    if(i < 0)
    return NULL;
    LNode* p;
    int j = 0;
    p = L;
    while(p != NULL && j < i) {
    p = p->next;
    j++;
    }
    return p;
    }

    平均时间复杂度:O(n)

  • 封装(基本操作)的好处:避免重复代码,简洁、易维护。

  • 按值查找,找到数据域 == e的结点。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
      LNode* LocateElem(LinkList L, ElemType e) {
    LNode* p = L->next;
    while(p != NULL && p->data != e)
    p = p->next;
    return p;
    }

    - 求表的长度。

    ```c
    int Length(LinkList L) {
    int len = 0;
    while(p->next != NULL) {
    p = p->next;
    len++;
    }
    }

    时间复杂度:O(1)

单链表的建立

  • 尾插法建立单链表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    LinkList List_TailInsert(LinkList& L) {
    int x;
    L = (LinkList)malloc(sizeof(LNode));
    LNode* s,* r = L;
    scanf("%d", &x);
    while(x != 9999) {//输入9999表示结束
    s = (LNode*)malloc(sizeof(LNode));
    s->data = x;
    r->next = s;
    r = s;
    scanf("%d", &x);
    }
    r->next = NULL;
    return L;
    }

    注意:设置一个指向表尾结点的指针。

  • 头插法建立单链表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
      LinkList List_HeadInsert(LinkList& L) {
    LNode *s;
    int x;
    L = (LinkList)malloc(sizeof(LNode));
    L->next = NULL;
    scanf("%d", &x);
    while(x != 9999) {//输入9999表示结束
    s = (LNode *)malloc(sizeof(LNode));
    s->data = x;
    s->next = L->next;
    L->next = s;
    scanf("%d", &x);
    }
    return L;
    }

    # 双链表

    - 单链表:无法逆向检索,有时候不太方便。

    - 双链表:可双向检索,存储密度略低于单链表。

    - ```c
    typedef struct DNode {
    ElemType data;
    struct DNode* prior, * next;
    }DNode, * DLinkList;
  • 双链表的初始化(带头结点)

    1
    2
    3
    4
    5
    6
    7
    8
    bool InitDLinkList(DLinklist& L) {
    L = (DNode*)malloc(sizeof(DNode));
    if(L == NULL)//内存不足,分配失败
    return false;
    L->prior = NULL;
    L->next = NULL;
    return true;
    }
  • 双链表的插入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    bool InsertNextDNode(DNode* p, DNode* s) {
    if(p == NULL || s == NULL)
    return false;
    s->next = p->next;//将s结点插入到p结点之后
    if(p->next != NULL)
    p->next->prior = s;
    s->prior = p;
    p->next = s;
    }
  • 双链表的删除

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    bool DeleteNextDNode(DNode* p) {//删除p结点的后继结点q
    if(p == NULL)
    return false;
    DNode* q = p->next;
    if(q == NULL)
    return false;
    p->next = q->next;
    if(q->next != NULL)
    q->next->prior = p;
    free(q);
    return true;
    }
  • 双链表的遍历

    • 后向遍历
    1
    2
    3
    4
    while(p != NULL) {
    //对结点p做相应处理
    p = p->next;
    }
    • 前向遍历
    1
    2
    3
    4
    while(p != NULL) {
    //对结点p做相应处理
    p = p->prior;
    }
    • 前向遍历(跳过头结点
    1
    2
    3
    4
    while(p->prior != NULL) {
    //对结点p做相应处理
    p = p->prior;
    }
  • 双链表不可随机存取,按位查找、按值查找操作都只能用遍历方法实现。时间复杂度O(n)

循环链表

  • 单链表:从一个结点出发只能找到后续的各个结点。

  • 循环单链表:从一个结点出发可以找到其他任何一个结点。

  • 初始化循环单链表

    1
    2
    3
    4
    5
    6
    7
    bool InitList(LinkList& L) {
    L = (LNode*)malloc(sizeof(LNode));
    if(L == NULL)//内存不足,分配失败
    return false;
    L->next = L;//头结点next指向头结点
    return true;
    }
  • 在循环单链表中如果需要经常对表头/表尾结点操作,可将表头/表尾设为头结点。

  • 初始化循环双链表

    1
    2
    3
    4
    5
    6
    7
    8
    bool InitDLinkList(DLinklist& L) {
    L = (DNode*)malloc(sizeof(DNode));
    if(L == NULL)
    return false;
    L->prior = L;
    L->next = L;
    return true;
    }
  • 循环双链表的插入

    1
    2
    3
    4
    5
    6
    bool InsertNextDNode(DNode* p, DNode* s) {
    s->next = p->next;//将s结点插入到p结点之后
    p->next->prior = s;
    s->prior = p;
    p->next = s;
    }
  • 循环双链表的删除

    1
    2
    3
    4
    5
    6
    7
    bool DeleteNextDNode(DNode* p) {//删除p结点的后继结点q
    DNode* q = p->next;
    p->next = q->next;
    q->next->prior = p;
    free(q);
    return true;
    }

静态链表

  • 定义:分配一整片连续的内存空间,各个结点集中安置(用数组方式实现的链表)。

    • 与数组不同之处:数组各个元素是按照数据下标0至n依次排序的。而在每个结点包含数据元素和下一个结点的数组下标(游标)两个信息,因而静态链表不一定依次排序。
    • 每个数据元素4B,每个游标4B(每个结点8B),设起始地址为addr(不是e0地址),则e1的存放地址为addr + 8*2。
  • 结构体定义

    1
    2
    3
    4
    5
    #define MaxSize 10
    struct Node {
    ElemType data;
    int next;//下一个数据下标
    }
    1
    2
    3
    4
    5
    #define MaxSize 10
    typedef struct {
    ElemType data;
    int next;
    }SLinkList[MaxSize]
    • SLinkList a可用 SLinkList 定义“一个长度为 MaxSize 的 Node 型数组”
  • 初始化静态链表:

    • 把 a[0] 的 next 设为-1。
    • 把其他结点的 next 设为一个特殊的值来表示空闲,如-2。
  • 查找:从头结点出发依次往后遍历结点。

  • 插入位序为i的结点:

    1. 找到一个空的结点,存入数据元素。
    2. 从头结点出发找到位序为 i-1 的结点。
    3. 修改新结点的 next。
    4. 修改 i-1 号结点的 next。
  • 删除某个结点:

    1. 从头结点出发找到前驱结点。
    2. 修改前驱结点的游标。
    3. 被删除结点的 next 设为-2。
  • 静态链表的优点:增删操作不需要大量移动元素

  • 静态链表的缺点:

    • 不能随机存取,只能从头结点开始依次往后查找。
    • 容量固定不可变。

顺序表和链表的比较

逻辑结构

  • 都属于线性表,都是线性结构。

存储结构

  • 顺序表:
    • 优点:支持随机存取、存储密度高。
    • 缺点:大片连续空间分配不方便,改变容量不方便。
  • 链表:
    • 优点:离散的小空间分配方便,改变容量方便。
    • 缺点:不可随机存取,存储密度低。

基本操作

  • 初始化:
    • 顺序表:需要预分配大片连续空间。若分配内存过小,则之后不方便拓展容量;若分配空间过大,则浪费内存资源。
      • 静态分配:静态数组:容量不可改变。
      • 动态分配:动态数组(malloc、free):容量可以改变,但需要移动大量元素,时间代价高。
    • 链表:只需分配一个头结点(也可以不要头结点,只声明一个头指针),以后方便拓展。
  • 销毁:
    • 顺序表:修改length=0。
      • 静态分配(静态数组):系统自动回收空间。
      • 动态分配(动态数组:malloc、free):需要手动 free。
    • 链表:依次删除各个结点(free)。
  • 插入删除:
    • 顺序表:需要将后续元素后移/前移。
      • 时间复杂度:O(n)。时间开销主要来自于移动元素。若数据元素很大,则移动的时间代价很高。
    • 链表:只需要修改指针。
      • 时间复杂度:O(n)。时间开销主要来自于查找元素。查找元素的时间代价更低。
  • 查找:
    • 顺序表:
      • 按位查找:O(1)
      • 按值查找:O(n)。若表内元素有序,则可在O(log2n)时间内找到。
    • 链表:
      • 按位查找:O(n)
      • 按值查找:O(n)
  • 表长难以预估、经常要增加/删除元素:链表。
  • 表长可预估、查询(搜索)操作较多:顺序表。