学习笔记:大话数据结构(一)

第一章 数据结构绪论


逻辑结构与物理结构:

  • 逻辑结构指数据对象中数据元素之间的相互关系;分为集合结构、线性结构、树形结构、图形结构四种;
  • 物理结构指数据的逻辑结构在计算机中的存储形式;分为顺序存储、链式存储两种;

第二章 算法


定义:解决特定问题求解步骤的描述,在计算机中变现为指令的有限序列,并且每条指令表示一个或多个操作。
特性:输入输出、有穷性、确定性、可行性
算法设计要求:正确性、可读性、健壮性、时间效率高和存储量低
效率度量方法:事后统计、事前分析估算

时间复杂度:算法语句总执行次数T(n)是关于问题规模n的函数,记作T(n)=O(f(n)),即大O阶。

推导大O阶:

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

常见时间复杂度:
常数阶O(1)<对数阶O(logN)<线性阶O(N)<O(N*logN)<平方阶O(N2)<立方阶O(N3)<乘方阶O(2N)<阶乘阶O(N!)<O(NN)

平均运行时间:期望的运行时间;
最坏运行时间:是一种运行时间的保证。通常,除非特别指定,提到的运行时间都是最坏运行时间;

第三章 线性表


线性表(List):零个或多个数据元素的有限序列;元素之间有序,若有多个元素,则第一个元素无前驱,最后一个元素无后继,其余每个元素都有且只有一个前驱和后继;
线性表长度:线性表元素的个数n(n>=0),当n=0时称为空表;
线性表数据元素:一个线性表的所有数据元素要相同类型的数据;复杂表中,一个数据元素可以有多个数据项组成;

线性表顺序存储结构:

指用一段地址连续的存储单元依次存储线性表的数据元素,可用一位数组实现。其实顺序存储结构就是一个数组的初始化,即声明一个类型和大小的数组并赋值的过程;需要三个属性:

  • 存储空间的起始位置:数组data的存储位置就是存储空间的存储位置;
  • 线性表的最大存储容量:数组长度MaxSize;
  • 线性表当前长度:length;

存储地址计算方法:存储器中的每个存储单元都有自己的编号,这个编号称为地址;

private int OK = 1;
private int ERROR = 0;
private int TRUE = 1;
private int FALSE = 0;
// 获取线性表第i个位置的元素,赋给e
DataType getEleFromList(List L, int i, DataType d) {
	if (L.length == 0 || i<1 || i > L.length) { // 判断空表或下标越界
		return ERROR;
	}
	d = L.data[i-1];
	return OK;
}

/**
 * 1. 插入位置不合理则返回异常;
 * 2. 线性表长度大于数组长度,则返回异常或者增加容量;
 * 3. (在不需要辅助内存,在原表操作的前提下)从最后一个元素开始向前遍历到第i个位置,分别都后移一位;
 * 4. 将要插入的元素填入位置i处;
 * 5. 线性表长度+1;
 * /
void insertEleToList(List L, int i, DataType d) {
	int k;
	if (L.length == MAXSIZE) { // 线性表已满
		return ERROR;
	}
	if (i<1 || i > L.length+1) { // i不在范围内
		return ERROR;
	}
	if (i<L.length) { // 插入位置不在表尾
		for (k = L.length - 1; k >= i-1; k--) {
			L.data[k+1] = L.data[k];
		}
	}
	L.data[i-1] = d;  // 插入新元素d 
	L.length++; // 表长+1
	return OK;
}

/**
 * 如果删除位置不合适,则返回异常;
 * 取出要删除的元素;
 * 从删除元素位置开始遍历到最后一个元素位置,分别把他们都向前移动一个位置;
 * 表长-1;
 * /
DataType deleteEleFromList(List L, int i, DataType d) {
	int k;
	if (L.length == 0) { // 线性表为空
		return ERROR;
	}
	if (i<1 || i>L.length) { // 删除位置不正确
		return ERROR;
	}
	d = L.data[i-1];
	if (i<L.length) { // 如果删除的不是最后位置
		for (k = i; k < L.length; k++) {
			L.data[k-1] = L.data[k]; // 将删除位置的后继元素前移
		}
	}
	L.length--; // 表长-1
	return OK;
}

总结:线性表的顺序存储结构,在存或读数据时时间复杂度都是O(1),而插入或删除数据时时间复杂度都是O(n);
优点:

  • 不需要为表示表中元素间的逻辑关系而额外增加存储空间;
  • 可以快速的存取表中任一位置的元素;

缺点:

  • 插入、删除操作需要移动大量元素;
  • 当线性表长度变化较大时,难以确定存储空间的容量;
  • 造成存储空间“碎片化”;

线性表的链式存储结构

线性表的链式存储结构特点:用一组任意的存储单元来存储线性表的结点,这组存储单元可以是连续的,也可以是不连续的;这就意味着这些元素可以在内存未被占用的任意位置。
与线性表顺序结构的区别:顺序结构中每个数据元素只需要存储数据元素信息即可,链式结构中,要存储数据元素信息和它的后继元素的内存地址;
数据域:存储数据元素信息的域;指针域或链:存储直接后继位置的域;数据域和指针域组成链表的存储映像,称为结点(Node);由多个结点组成链表,因为每个结点只包含一个指针域,又称为单链表;
第一个结点的存储位置称为头指针;最后一个结点指针为空(用Null或^表示);有时为了方便对链表进行操作,会在单链表的第一个结点前附设一个结点称为头结点,头结点的数据域可以不存储任何信息,也可以一般存储线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针;

头指针:

  • 头指针是指链表指向第一个结点的指针,若链表有头结点,则头指针是指向头结点的指针;
  • 头指针具有标识作用,所以常用头指针冠以链表的名字;
  • 无论链表是否为空,头指针均不为空,头指针是链表的必要元素;

头结点:

  • 头结点是为了操作链表的统一和方便而设立的,放在第一元素结点的前面,其数据域一般无意义(也可存放链表长度);
  • 有了头结点,要在第一结点前插入结点和删除第一结点就很方便了,其操作与其他结点的操作就统一了;
  • 头结点不一定是链表必须要素;

LinkedListNode node = new LinkedListNode();
node.data 表示该结点数据域存储的数据信息;node.next表示该结点指针域存储的地址值;

/**
 * 获取单链表第i个数据:getEleFromLinkedList()
 * 1. 声明一个指针current指向链表的第一个结点,初始化计数器j从1开始;
 * 2. 当j<i时就遍历链表,让指针current向后移动,不断指向下一结点,j累加1;
 * 3. 若到链表末尾current为空,则说明第i个结点不存在;
 * 4. 否则查找成功,返回指针current指向结点的数据;
 */
DataType getEleFromLinkedList(LinkedListNode head, int i, DataType d) {
	int j = 1; // 计数器
	LinkedLinstNode current = head; // 当前指针current指向表头head
	while (current != null && j<i) { // 指针current不为空,且计数器j<i时,继续循环
		current = current.next;
		++j;
	}
	if (current == null || j>i) { // 指针current为空,或计数器j>i时,则第i个结点不存在,返回异常
		return ERROR;
	}
	d = current.data; // 接收第i个结点数据值
	return OK; // 返回查找成功
}

/**
 * 单链表第i个位置插入结点insertNodeToLinkedList(LinkedListNode head, int i, DataType d)
 * 1. 声明一个指针current指向链表的第一个结点,初始化计数器j从1开始;
 * 2. 当j<i时就遍历链表,让指针current向后移动,不断指向下一结点,j累加1;
 * 3. 若到链表末尾current为空,则说明第i个结点不存在;
 * 4. 否则查找成功,创建一个空节点node并将要插入额数据d赋值给node,即node.data = d;
 * 5. 执行单链表插入语句:node.next = current.next; current.next = node;
 */
DataType insertNodeToLinkedList(LinkedListNode head, int i, DataType d) {
	int j = 1;
	LinkedListNode current = head;
	while (current != null && j<i) { // 单链表不为空且还未遍历带第i个结点,继续循环
		current = current.next;
		++j;
	}
	if (current == null || j>i) { // 单链表为空或第i个结点不存在,返回异常
		return ERROR;
	}
	LinkedListNode node = new LinkedListNode(d); // 传入参数d,创建要插入的结点
	node.next = current.next; // 把当前结点的指针域赋给新建的空结点node
	current.next = node; // 把当前结点额指针域指向新建的空结点node,千万注意这两行顺序不能颠倒
	return OK;
}

/**
 * 删除单链表第i个数据:deleteNodeFromLinkedList()
 * 1. 声明一个指针current指向链表的第一个结点,初始化计数器j从1开始;
 * 2. 当j<i时就遍历链表,让指针current向后移动,不断指向下一结点,j累加1;
 * 3. 若到链表末尾current为空,则说明第i个结点不存在;
 * 4. 否则查找成功,把第i个结点后继元的地址值赋值给第i个结点前驱元的指针域;
 * 5. 把第i个结点存储的数据赋值给变量d并返回,此时系统会回收第i个结点的内存;
 */
DataType deleteNodeFromLinkedList(LinkedListNode head, int i, DataType d) {
	int j = 1;
	LinkedListNode current = head;
	while (current.next != null && j<i-1) { // 单链表不为空且j<i-1,继续循环
		current = current.next;
		++j;
	}
	if (current.next == null && j>i) { // 单链表为空或者不存在第i个结点,返回异常
		return ERROR;
	}
	current.next = current.next.next; // 把第(i-1)个结点的后继元的后继元地址值赋值给第(i-1)个结点的指针域
	d = current.next.data; // 接收第i个结点数据并返回
	return d;
}

单链表插入、删除结点的总结:对于插入或删除结点越频繁的操作,单链表的效率就越高;

单链表的整表创建

/**
 * 单链表整表创建:头插法createLinkedListFromHead(int n)
 * 1. 声明一个指针current和计数变量i;
 * 2. 初始化一个空链表list,list头结点指向null,即创建一个带头结点的空表;
 * 3. 循环:生成一新结点赋值给current;随机生成一数字赋值给current的数据域current.data;
 * 4. 将current插入到头结点与前一新节点之间;
 */
void createLinkedListFromHead(int n) {
	LinkedListNode head = new LinkedListNode(); // 创建头结点
	head.next = null;
	node.next = null;
	for (int i = 0; i<n; i++) {
		LinkedListNode current = new LinkedListNode(); // 创建要插入的结点
		int num = Math.random/100 + 1;// 生成100以内的随机数num
		current.data = num; // 赋值给current.data
		current.next = head.next; // 把头结点的指针域赋值给新结点
		head.next = current; // 新节点始终插入到头结点的后面,即第一个结点的位置
	}
}

/**
 * 单链表整表创建:尾插法createLinkedListFromTail(int n)
 */
void createLinkedListFromTail(int n) {
	int num = Math.random/100 + 1;// 生成100以内的随机数num
	LinkedListNode tail = new LinkedListNode(); // 创建尾结点
	for (int i = 0; i<n; i++) {
		LinkedListNode current = new LinkedListNode(); // 创建要插入的结点
		int num = Math.random/100 + 1;// 生成100以内的随机数num
		current.data = num; // 赋值给current.data
		tail.next = current;
		tail = current; // 让新结点变成新的尾结点
	}
	tail.next = null; // 尾结点指针域置空
}

单链表的整表删除

当我们不需要使用这个单链表时,需要把它销毁掉,也就是在内存中将他释放掉。

/**
 * 单链表整表删除
 * 1. 创建两个结点current和runner
 * 2. 把第一个结点赋给current
 * 3. 循环:将下一个结点赋值给runner;释放current;将runner赋给current;
 */
void clearAllLinkedList(LinkedList list) {
	LinkedListNode current = new LinkedListNode();
	LinkedListNode runner = new LinkedListNode();
	current = list.next;
	while (current != null) {
		runner = current.next;
		/*释放current结点 free(current)*/
		current = runner;
	}
	list.next = null;
}

单链表存储结构与顺序存储结构的对比:

  • 存储分配方式:
    • 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素;
    • 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素;
  • 时间性能:
    • 查找:
      • 顺序结构O(1)
      • 单链表O(n)
    • 插入和删除:
      • 顺序存储结构需要平均移动表长一半的元素,时间为O(n)
      • 单链表在给出某位置的指针后,插入、删除的时间仅为O(1)
  • 空间性能:
    • 顺序存储结构需要预分配存储空间,分多了造成内存浪费,分少了已发生内存上溢
    • 单链表不需要分配存储空间,只要有剩余空间就可以分配,结点个数不受限制

得出结论:

  • 如果线性表需要频繁查找,很少进行插入和删除操作的时候,宜采用顺序存储结构;若需要频繁插入和删除的时候,宜采用单链表结构;
  • 当线性表中的元素个数变化较大或者根本不知道有多大时,最好用暗单链表结构,这样可以不用考虑存储空间的大小问题;而如果事先知道线性表的大致长度,比如一星期就是7天,一年就是12个月,这些情况用顺序存储结构效率就会高出很多。

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

C语言通过指针可以非常容易的操作内存中的地址和数据;Java等面向对象语言启用对象引用机制间接实现了指针的某些作用。

让数组的每个元素都有两个数据域组成,data和cur,也就是数组每个元素的下标都对应一个data和cur。数据域data用来存放数据元素,也就是我们要处理的数据;数据域cur相当于单链表的指针,用来存放该元素的后继在数组中的下标,我们把cur叫做游标!我们通常会把数组创建的大些,以避免插入较多数据时不至于造成溢出。

另外对数组的第一个元素和最后一个元素做特殊处理,不存数据。把未被使用的数组元素称为备用链表。而数组第一个元素,即下标为0的元素的cur存放备用链表的第一个数组元素的下标;而最后一个元素的cur存放第一个有存储数据的数组元素的下标,相当于单链表中头结点的作用;有存储数据的最后一个数组元素的cur存放0表示下一个数组元素为空;

/**
 * 创建(初始化)静态链表
 */
void initStaticLinkedList(StaticLinkedList list, int listSize) {
	for (int i = 0; i<listSize-1; i++) {
		list[i].cur = i+1;
	}
	list[listSize-1] = 0; // 最后那个数组元素的cur为0,表示数组的第一个元素
	return SUCCESS; // 目前静态链表为空
}

静态链表要解决的问题:如何用静态链表模拟动态链表结构的存储空间的分配:需要时申请,无用时释放?
解决办法是:把所有未被使用的及已被删除的结点用游标链成一个备用的链表,每当需要插入数据时,就可以从备用链表上获取第一个结点作为待插入的结点。

/*若备用链表非空则返回分配的结点下标,否则返回0*/
int applyMemory(StaticLinkedList list) {
	int i = list[0].cur; // 获取当前静态链表小标为0的元素的cur值,也就是要返回的第一个备用的空闲下标 
	if (list[0].cur != null) {
		list[0].cur = list[i].cur; // 由于第一个备用链表的第一个结点被拿来用了,就把它的cur值赋给lis								   //[0].cur来记录
	}
	return i;
}

/*静态链表插入结点实现,在位置i插入数据d*/
void insertToStaticLinkedList(StaticLinkedList list; int i; DataType d) {
	int j=list[0].cur, k=list.length-1; // j为第一个空闲元素的下标,k为最后一个元素的下标 
	if (i<1 || i>list.length) {
		return ERROR;
	}
	if (j != null) { // 表示有空闲元素,继续执行
		list[j].data = d; // 把要插入的数据赋给第一个空闲元素的data,即list[j].data
		for (l=1; l<=i-1; l++) { // 找到位置i的前一个元素
			k = list[k].cur;
		}
		list[j].cur = list[k].cur; // 把位置(i-1)元素的cur值赋给新插入结点的cur,list[j].cur
		list[k].cur = j; // 把新插入结点的下标赋给位置(i-1)元素cur
		return OK;
	}
	return ERROR;
}

/*静态链表的删除操作,删除静态链表位置i的结点*/
DataType deleteFromStaticLinkedList(StaticLinkedList list, int i) {
	DataType d = null;
	int j, k=list.length-1;
	if (i<1 || i>list.length) {
		return ERROR;
	}
	for (j=1; j<=i-1; j++) {
		k = list[k].cur; // k就是位置i结点的下标值,此时j=i-1
	}
	j = list[k].cur; // 把位置i结点存储的cur值(也即位置为(i+1)结点的下标值)赋给变量j
	list[k].cur = list[j].cur; // 位置为i结点存储的cur值,赋给位置(i-1)结点的cur值
	/*free(list[j]),即将结点j回收到备用链表*/
	
	d = list[k].data; // 接收结点i的data值并返回
	
	list[k].cur = list[0].cur; // 此时k=i,要将结点i回收,就要把结点i插入到第一个备用结点的位置,把list							   //[0].cur赋给list[k].cur
	list[0].cur = k; // 然后让list[0].cur存储k值,即结点i的下标
	return d;
}

/*获取静态链表的长度*/
int getStaticLinkedListLength(StaticLinkedList list) {
	int length = 0;
	int i = list[SIZE-1].cur; // 拿到第一个存储数据的结点下标值
	while (i != null) {
		i = list[1].cur;
		length++;
	}
	return legnth;
}

静态链表的优缺点:

  • 优点:在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中插入删除元素需要移动大量数据的缺点;
  • 缺点:1. 没有解决连续存储分配导致表长难以确定的问题;2. 失去了顺序存储结构随机存储的特性;

单循环链表

将单链表的终端结点的指针端由空指针改为指向头结点,就是单链表形成一个首尾相接的环形,称为单循环链表,简称循环链表。

指向终端结点的指针是尾指针rear,则可以很方便的访问终端结点(rear)和头结点(rear.next)了,访问终端结点的时间是O(1),访问头结点的时间是O(2);此时可以很方便的把两个循环链表链接成一个循环链表:

int listAHead = rearA.next; // listAHead用来保存A链表的头结点
rearA.next = rearB.next.next; // 把B链表的第一个结点(即rearB.next.next)赋值给A链表的尾结点指针
int listBHead = rearB.next; // listBHead用来保存链表B的头结点
rearB.next = listAHead; // 让链表B的尾结点指针指向A链表的头结点
/*free(listBHead)释放链表B的头结点内存*/

双向链表

双向链表是在单链表的每个结点再加一个指针域指向其前驱结点,所以双向链表的每个结点都有两个指针域,一个指针域指向其后继结点,另一个指针域指向其前驱结点。

双循环链表

双向循环空链表的只有一个头结点,其头指针指向头结点,前驱指针指向头结点,后继指针也指向头结点;

循环非空带头结点双链表的看似复杂,但是其基本操作和单链表是一样的,我们只要使用一个方向的指针即可;另一方面,由于其具有两个方向的指针,那么就可以反向遍历链表,这很方便查找操作,但是增删操作却更麻烦了,删除操作之前需要更改两个指针变量,插入操作时顺序千万不能写反!

/*在结点p和结点p.next之间插入结点s*/
s.prev = p; // 1. 搞定s结点的前驱
s.next = p.next; // 2. 搞定s结点后继
p.next.prev = s; // 3. 搞定后结点的前驱
p.next = s; // 4. 搞定前结点的后继
/*千万注意2、3、4步的顺序不能写反,因为2、3步需要用到p.next,如果先执行第4步为p.next赋值的操作,就会导致不能插入新结点*/

/*删除结点p*/
p.prev.next = p.next; // 把p的后继指针赋值给p前驱元的后继指针
p.next.prev = p.prev; // 把p的前驱指针赋值给p后继元的前驱指针
/*free(p)释放链表B的头结点内存*/

posted @ 2016-04-04 10:56  布鲁克林一棵树  阅读(450)  评论(0编辑  收藏  举报