线性表的顺序表示 数组静态分配。
#define MaxSize 50 typedef struct { int data[MaxSize]; int length; } SqlList;
课后练习 1、从顺序表中删除具有最小值的元素(假设唯一)并由函数返回被删元素的值。空出的位置由最后一个元素填补,若顺序表为空则显示出错信息并退出运行。
算法思路 :搜索整个顺序表,查找最小值元素并记住其位置 。搜索结束后,用最后一个元素填补空出的原最小值元素的位置。
bool Del_Min (SqList* L, int * value) { if (L->length == 0 ) { return false ; } value = L->data[0 ]; int pos = 0 ; for (int i = 1 ; i < L->length; i++) { if (L->data[i] < value) { value = L->data[i]; pos = i; } } L->data[pos] = L->data[L->length - 1 ]; L->length--; return true ; }
2、设计一个高效算法,将顺序表L的所有元素逆置,要求算法的空间复杂度为O(1)。
算法思路 :扫描顺序表L的前半部分元素 ,对于元素L.data[i]将其与后半部分的对应元素 L.data[L.length - i - 1]进行交换。
void Reverse (SqList* L) { int temp; for (int i = 0 ; i < L->length / 2 ; i++) { temp = L->data[i]; L->data[i] = L->data[L->length - 1 - i]; L->data[L->length - 1 - i] = temp; } }
3、对于长度为n的顺序表L,编写一个时间复杂度为O(n)、空间复杂度为O(1)的算法,该算法删除线性表中所有值为x的数据元素。
算法思路 :用k记录 顺序表L中不等于x的元素个数 (即需要保存的元素个数),边扫描L边统计k ,并将不等于x的元素前移k个位置 ,最后修改L的长度 。
理解 :要想到用k下标来接收部位x的值,即重新复制L.data[k],最后更新length。不要逐个前移。
void del_x (SqList* L) { int k = 0 ; for (int i = 0 ; i < L->length; i++) { if (L->data[i] != 'x' ) { L->data[k] = L->data[i]; k++; } } L->length = k; }
4、从有序顺序表中删除其值在给定值s与t之间(要求s < t)的所有元素,如果s或t不合理或顺序表为空,则显示出错信息并退出运行。
算法思路 :先寻找值大于等于s的第一个元素 (第一个删除的元素),然后寻找值大于t的第一个元素 (最后一个删除的元素的下一个元素),要将这段元素删除,只需直接将后面的元素前移。
注意 :因为是有序表 ,所以删除的元素必然是相连的整体 。
bool Del_s_t2 (SqList* L, int s, int t) { int i, j; if (s >= t || L->length == 0 ) { return false ; } for (i = 0 ; i < L->length && L->data[i] < s; i++); if (i >= L->length) { return false ; } for (j = i; j < L->length && L->data[j] <= t; j++); for (; j < L->length; i++, j++) { L->data[i] = L->data[j]; } L->length = i; return true ; }
5、从顺序表中删除其值在给定值s与t之间(包含s和t,要求s < t)的所有元素,如果s或t不合理或顺序表为空,则显示出错信息并退出运行。
算法思路 :从前向后扫描顺序表L,用k记录 元素值在s到t之间元素的个数(初始k=0)。对于当前扫描的元素,若其值不在s到t之间,则前移k个位置;否则执行k++。
优点 :每个不在s到t之间的元素仅移动一次 ,算法效率高 。
理解 :也是借助k记录位置,重新对L.data[i]赋值。
bool Del_s_t (SqList* L, int s, int t) { int k = 0 ; if (s >= t || L->length == 0 ) { return false ; } for (int i = 0 ; i < L->length; i++) { if (L->data[i] >= s && L->data[i] <= t) { k++; } else { L->data[i - k] = L->data[i]; } } L->length = L->length - k; return true ; }
6、从有序顺序表中删除所有其值重复的元素,使表中所有元素的值均不同。
算法思路 :用类似于直接插入排序的思想,初始时将第一个元素 视为非重复的有序表 。之后依次判断后面的元素 是否与前面非重复有有序表的最后一个元素相同,若相同则继续向后判断,若不同则插入到前面的非重复有序表的最后,直至判断到表尾为止。
注意 :因为是有序顺序表 ,值相同的元素一定在连续的位置上 。
bool Delete_Same (SqList* L) { if (L->length == 0 ) { return false ; } int i, j; for (i = 0 , j = 1 ; j < L->length; j++) { if (L->data[i] != L->data[j]) { L->data[i++] = L->data[j]; } } L->length = i + 1 ; return true ; }
7、将两个有序顺序表合并为一个新的有序顺序表,并由函数返回结果顺序表。
算法思路 :首先,按顺序不断取下两个顺序表表头较小的节点 存入新的顺序表中。然后,看哪个表还有剩余 ,将剩下的部分 加到新的顺序表后面 。
理解 :两个有序表逐个元素比较,把小的值插入到新的顺序表。搞完一个表后,另一个表剩下的部分就直接插入到新的顺序表的后面。
注意 :本算法非常典型,需牢固掌握。
bool Merge (SqList A, SqList B, SqList* C) { if (A.length + B.length > C->MaxSize) { return false ; } int i = 0 , j = 0 , k = 0 ; while (i < A.length && j < B.length) { if (A.data[i] <= B.data[j]) { C->data[k++] = A.data[i++]; } else { C->data[k++] = B.data[j++]; } } while (i < A.length) { C->data[k++] = A.data[i++]; } while (j < B.length) { C->data[k++] = B.data[j++]; } C->length = k; return true ; }
8、已知在一维数组A[m + n]中依次存放两个线性表(a1, a2, a3, …, am)和(b1, b2, b3, …, bn)。试编写一个函数,将数组中两个顺序表的位置互换,即将(b1, b2, b3,…, bn)放在(a1, a2, a3, …, am)的前面。
算法思路 :先将数组A[m + n]中的全部元素 (a1, a2, a3, …, am, b1, b2, b3, …, bn)原地逆置 为(bn, bn-1, bn-2, …, b1, am, am-1, am-2, …, a1),再对前n个元素 和后m个元素 分别使用逆置算法 ,即可得到(b1, b2, b3, …, a1, a2, a3, …, am),从而实现顺序表的位置互换。
void Reverse (int A[], int left, int right, int arraySize) { if (left >= right || right >= arraySize) { return ; } int mid = (left + right) / 2 ; for (int i = 0 ; i < mid - left; i++) { int temp = A[left + i]; A[left + i] = A[right - i]; A[right - i] = temp; } } void Exchange (int A[], int m, int n, int arraySize) { Reverse(A, 0 , m + n - 1 , arraySize); Reverse(A, 0 , n - 1 , arraySize); Reverse(A, n, m + n - 1 , arraySize); }
9、线性表(a1, a2, a3, …, an)中的元素递增有序且按顺序存储于计算机内。要求设计一算法,完成用最少时间在表中查找数值为x的元素,若找到则将其与后继元素位置相交换,若找不到则将其插入表中并使表中元素仍递增有序。
算法思路 :顺序存储的线性表递增有序 ,可以顺序查找 ,也可以折半查找 。题目要求“用最少的时间”,这里应使用折半查找法。
oid SearchExchangeInsert (int A[], int n, int x) { int low = 0 , high = n - 1 , mid; while (low <= high) { mid = (low + high) / 2 ; if (A[mid] == x) { break ; } else if (A[mid] < x) { low = mid + 1 ; } else { high = mid - 1 ; } } if (A[mid] == x && mid != n - 1 ) { int temp = A[mid]; A[mid] = A[mid + 1 ]; A[mid + 1 ] = temp; } if (low > high) { int i; for (i = n - 1 ;i > high;i--) { A[i + 1 ] = A[i]; } A[i + 1 ] = x; } }
线性表的链式表示 单链表 线性表的链式存储 又称为单链表 。其中data为数据域 ,存放数据元素,next为指针域 ,存放其后继结点的地址。
单链表是非随机存取 的存储结构,即不能直接找到表中某个特定的结点。查找某个特定的结点时,需要从表头开始遍历 ,依次查找。
单链表的定义如下。
typedef struct LNode { int data; struct LNode * next ; }LNode, * LinkList;
增加一个新的结点:在内存中申请一个结点所需空间,并用指针p指向这个结点。
struct LNode * p = (struct LNode *)malloc (sizeof (struct LNode )); LNode* p1 = (LNode*)malloc (sizeof (LNode));
单链表又分为带头结点 和不带头结点 。带头结点写代码方便,不带头结点写代码麻烦。
初始化一个空的单链表。
bool InitList (LinkList L) { L = NULL ; return true ; } bool InitList (LinkList L) { L = (LNode*)malloc (sizeof (LNode)); if (L == NULL ) return false ; L->next = NULL ; return true ; }
判断单链表是否为空。
bool Empty (LinkList L) { return (L == NULL ); } bool Empty (LinkList L) { return (L->next == NULL ); }
单链表的优点 :不要求大片连续空间,改变内容方便。
单链表的缺点 :不可以随机存取,要耗费一定空间存放指针。
C语言的typedef关键字 C语言的typedef关键字,用于数据类型重命名 。
语法:typdef <数据类型> <别名>
typedef int zhengshu;typedef int * zhengshuzhizhen;int x = 1 ; int * p;
单链表上基本操作的实现 按位序插入(带头结点) 最好时间复杂度是O(1) ,平均、最坏时间复杂度是O(n) 。
bool ListInsert (LinkList L, int i, int 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 ; }
按位序插入(不带头结点) 如果不带头结点,则插入、删除第1个元素时,需要更改头指针L 。
bool ListInsert (LinkList L, int i, int 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 ; } int j = 1 ; }
因此,不带头结点的话,代码写起来更加麻烦,推荐写代码时带头结点 。
指定结点的后插操作 时间复杂度是O(1) 。
bool InsertNextNode (LNode* p, int 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 ; }
指定节点的前插操作(偷天换日法) 时间复杂度是O(1) 。
bool InsertPriorNode (LNode* p, int 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->data = p->data; p->data = e; return true ; }
按位序删除(带头结点) 最好时间复杂度是O(1) ,平均、最坏时间复杂度是O(n) 。
bool ListDelete (LinkList L, int i, int 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 ; if (p->next == NULL ) return false ; LNode* q = p->next; e = q->data; p->next = q->next; free (q); return true ; }
删除指定结点p(偷天换日法) 删除结点p,需要修改其前驱结点的next。
方法1:传入头指针 ,循环寻找p的前驱结点。
方法2:偷天换日(类似于结点的前插的实现)。时间复杂度为O(1) 。
bool DeleNode (LNode* p) { if (p == NULL ) return false ; LNode* q = p->next; p->data = p->next->data; p->next = q->next; free (q); return true ; }
按位查找(带头结点) 平均时间复杂度是O(n) 。
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; }
封装后的按位插入 bool ListInsert (LinkList L, int i, int e) { if (i < 1 ) return false ; LNode* p = GetElem(L, i - 1 ); return InsertNextNode(p, e); }
按值查找 时间复杂度为O(1) 。
LNode* LocateElem (LinkList L, int e) { LNode* p = L->next; while (p != NULL && p->data != e) { p = p->next; } return p; }
求表的长度 时间复杂度为O(n) 。
int Length (LinkList L) { int len = 0 ; LNode* p = L; while (p->next != NULL ) { p = p->next; len++; } return len; }
尾插法建立单链表 时间复杂度为O(n) 。尾插法可以省略L->next = NULL; 这一句初始化,因为后续会改变L的指向。但是,编程时,最好都要进行初始化,防止使用脏数据 。
LinkList List_TailInsert (LinkList L) { int x; L = (LinkList)malloc (sizeof (LNode)); LNode* s, * r = L; scanf ("%d" , &x); while (x != 9999 ) { s = (LNode*)malloc (sizeof (LNode)); s->data = x; r->next = s; r = s; scanf ("%d" , &x); } r->next = NULL ; return L; }
头插法建立单链表 时间复杂度为O(n) 。头插法的重要应用是链表的逆置 。
LinkList List_HeadInsert (LinkList L) { LNode* s; int x; L = (LinkList)malloc (sizeof (LNode)); L->next = NULL ; scanf ("%d" , &x); while (x != 9999 ) { s = (LNode*)malloc (sizeof (LNode*)); s->data = x; s->next = L->next; L->next = s; scanf ("%d" , &x); } return L; }
双链表 双链表克服了单链表的缺点,可以双向访问。
双链表定义。
typedef struct DNode { int data; struct DNode * prior , * next ; } DNode, * DLinkList;
初始化双链表
bool InitDLinkList (DLinkList L) { L = (DNode*)malloc (sizeof (DNode)); if (L == NULL ) return false ; L->prior = NULL ; L->next = NULL ; return true ; }
判断双链表是否为空(带头结点)
bool Empty (DLinkList L) { return (L->next == NULL ); }
双链表的插入操作(后插操作) bool InsertNextDNode (DNode* p, DNode* s) { if (p == NULL || s == NULL ) return false ; s->next = p->next; if (p->next != NULL ) { p->next->prior = s; } s->prior = p; p->next = s; return true ; }
注意 :
修改指针要注意顺序合法。
如果p是最后一个结点,则p->next指向NULL ,会出现空指针异常,因此需要先判断p->next是否为空 。
按位序插入、前插操作,都可以转换为后插操作去实现。
按位序插入思路 :从头结点开始遍历,找到某个位序的前驱结点 ,对这个结点进行后插操作 。
前插操作思路 :找到给定结点的前驱结点 ,在对该结点的前驱结 点进行后插操作 。
双链表的删除操作 bool DeleteNextNode (DNode* p) { 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 ; }
注意 :如果p是最后一个结点,也需要先判断p->next是否为空 。
销毁一个双链表 void DestoryList (DLinkList L) { while (L->next != NULL ) { DeleteNextNode(L); } free (L); L = NULL ; }
双链表的遍历 while (p != NULL ){ p = p->next; } while (p != NULL ){ p = p->prior; } while (p->prior != NULL ){ p = p->prior; }
注意 :双链表不可随机存取。按位查找 、按值查找 都只能用遍历的方式实现。时间复杂度为O(n) 。
循环链表 循环单链表 初始化一个循环单链表。
bool InitList (LinkList L) { L = (LNode*)malloc (sizeof (LNode)); if (L == NULL ) return false ; L->next = L; return true ; }
判断循环单链表是否为空。
bool Empty (LinkList L) { return (L->next == L); }
判断节点p是否为循环单链表的表尾结点。
bool IsTail (LinkList L, LNode* p) { return (p->next == NULL ); }
循环双链表 初始化空的循环双链表。
bool InitDLink (DLinkList L) { L = (DNode*)malloc (sizeof (DNode)); if (L == NULL ) return false ; L->prior = L; L->next = L; return true ; }
判断循环双链表是否为空。
bool Empty (DLinkList L) { return (L->next == L); }
判断结点p是否为循环双链表的表尾结点。
bool IsTail (DLinkList L, DNode* p) { return (p->next == L); }
循环双链表的处插入:实现代码和双链表的后插操作 一样。但是不需要判断p->next是否为空 。
循环双链表的删除:代码和双链表的删除 一样。同样也不需要判断p->next是否为空 。
静态链表 静态链表是用数组方式 实现的链表 。
优点 :增、删操作不需要大量移动元素。
缺点 :不能随机存取 ,只能从头结点开始依次往后查找;容量固定不变 。
用代码定义一个静态链表。
#define MaxSize 10 typedef struct { int data; int next; } SLinkList[MaxSize];