目录
顺序表
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
为什么要写一个顺序表,直接写一个数组不就可以了吗?
不一样将数组和其方法写到类里,把其称为顺序表。后续就可直接用顺序表面向对象,而不用重复定义。
定义一个顺序表
顺序表可以分为
静态顺序表:使用定长数组存储
动态顺序表:使用动态开辟数组存储
把顺序表定义为一个类
public class MyArrayList {
public int[] elem;
public int usedSize;//有效的数据个数
//构造方法
public MyArrayList(){
this.elem = new int[10]
}
}
顺序表的接口实现
1、打印顺序表
如同遍历数组,打印元素一样。
public void display() {
if (this.usedSize == 0){
System.out.print("顺序表为空");
}
for (int i = 0;i < this.usedSize;i++){
System.out.print(this.elem[i] + " ");
}
}
2、获取顺序表的长度
有效数据的个数代表顺序表的长度
public int size() {
return this.usedSize;
}
3、判断顺序表是否饱和
当顺序表有效数据个数和开辟数组的长度相等时,顺序表饱和
public boolean isFull(){
return this.elem.length == this.usedSize;
}
4、查找顺序表位置对应的元素
1.判定查找位置是否合法,防止数组越界
2.遍历顺序表到查找位置,放回对应元素
//获取 pos 位置的元素
public int getPos(int pos) {
if (pos < 0 || pos > this.usedSize) {
System.out.println("pos的位置错误");
return -1;
}else{
return this.elem[pos];
}
}
5、查找某个元素对应的位置
遍历顺序表逐个比较,当顺序表的元素和查找元素相等,放回其数组下标
//查找某个元素对应的位置
public int search(int toFind) {
for (int i = 0;i < this.usedSize;i++){
if (this.elem[i] == toFind){
return i;
}
}
return -1;
}
当顺序表中元素为有序的,也可采用二分查找的方法,提高查找效率
//查找某个元素对应的位置(用二分查找法)
public int search2(int toFind) {
int left = 0;
int right = this.usedSize-1;
while (left < right){
int mid = (left + right) / 2;
if (this.elem[mid] < toFind){
left = mid + 1;
}else if(this.elem[mid] > toFind) {
right = mid - 1;
}else {
return mid;
}
}
System.out.println("没有找到");
return -1;
}
6、删除第一次出现的关键字key
1.用关键字key找到下标pos
2.遍历顺序表使得pos后的元素向前覆盖,达到删除效果
//删除第一次出现的关键字key
public void remove(int toRemove) {
int pos = search(toRemove);
for (int i = pos;i < this.usedSize;i++){
this.elem[i] = this.elem[i + 1];
}
this.usedSize--;
}
7、在pos位置增加元素
1.判断pos位置是否合法,防止数组越界
2.判断顺序表是否已满,已满需扩容
3.将pos位置的元素整体后移,再将要插入元素插入
// 在 pos 位置新增元素
public void add(int pos, int data) {
if (pos < 0 || pos > this.usedSize){
System.out.println("pos位置错误");
}
if (isFull()){
this.elem = Arrays.copyOf(this.elem,this.elem.length * 2);
}
for (int i = this.usedSize - 1;i >= pos;i--){
this.elem[i+1] = this.elem[i];
}
this.elem[pos] = data;
this.usedSize++;
}
8.清空顺序表
//清空顺序表
public void clear() {
//简单清除
this.usedSize = 0;
/*for (int i = 0;i < this.usedSize;i++){
this.elem[i] = 0;
}
this.usedSize = 0;*/
}
总结:
1.顺序表插入和删除元素必须得移动元素,时间复杂度达到O(n)。优化:必须要查找元素,但是把插入和删除做到O(1)
2.扩容也存在问题,比如:本身有10个空间,想插入第11个;扩容却扩到20,造成9个空间的浪费。优化:随用随取
链表
链表是一种物理存储结构上非连续的存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的。
实际,链表的组合形式有很多种,以下情况组合起来就有8种了,
带头,不带头
单向,双向
循环,非循环
重点掌握:不带头单向非循环和不带头双向非循环
链表的组成及定义
组成:链表由多个节点通过next域存储地址连接起来。
定义:
class ListNode{
public int val;
public ListNode next;
//构造方法
public ListNode(int data){
this.val = data;
}
}
其中next域存储下一节点的地址,
节点在内存的存储
当我们实例化一个节点时:ListNode listNode = new ListNode(12) ;
单向不带头不循环链表的连接情况
单向不带头非循环链表接口的实现
1.打印单向链表
1.定义一个ListNode类型的cur移动引用
2.cur在链表上逐个移动,直到为空
3.打印cur.val
//打印
public void display(){
ListNode cur = this.head;
while (cur != null){
System.out.print(cur.val + " ");
cur = cur.next;
}
}
2.头插法
1.实例化一个节点对象,体现了链表随用随取的思想
2.新节点链接链表
3.新节点为新头
//头插
public void addFirst(int data){
ListNode node = new ListNode(data);
node.next = head;
head = node;
}
3.尾插法
1.考虑到链表链表可能为空和不为空两种情况
2.当链表为空时,将node赋给head,node的next域被赋为null
3.当链表不为空,先找到链表的尾节点,链接新节点node,node的next域赋null
//尾插
public void addLast(int data){
ListNode node = new ListNode(data);
if (this.head == null){
this.head = node;
node.next = null;
}else {
ListNode cur = this.head;
while (cur.next != null){
cur = cur.next;
}
cur.next = node;
node.next = null;
}
}
4.得到链表的长度
1.定义一个计数变量count,用于统计链表长度
2.实例化一个移动引用cur指向头节点,当cur不为空,cur逐个移动,加1;后返回count
//得到单链表的长度
public int size(){
int count = 0;
ListNode cur = this.head;
while (cur != null){
count++;
cur = cur.next;
}
return count;
}
5.在任意位置插入节点
1.判定插入位置是否正确,不正确跳出方法。
2.链表坐标默认从0开始,如果插入位置为0,为头插;插入位置为链表长度,则为尾插。
3.若为中间插入,需实例化一个移动引用指向头节点;再逐个移动直到要插入位置的前一个节点;在通过改变指向链接节点。
//任意位置插入节点
public void addPos(int pos,int data){
ListNode node = new ListNode(data);
if (pos < 0 || pos > size()){
System.out.println("插入位置错误");
return;
}
else if (pos == 0){
addFirst(data);
}
else if (pos == size()){
addLast(data);
}else {
ListNode cur = this.head;
while (pos - 1 > 0){
cur = cur.next;
pos--;
}
node.next = cur.next;
cur.next =node;
}
}
6.找到关键字为key的节点的前驱
1.实例化一个移动引用cur先指向头节点
2.当cur下一节点不为null,cur逐个移动,直到cur下一节点val与key相等,返回cur
//找到插入位置前驱
public ListNode searchPre(int key){
ListNode cur = this.head;
while (cur.next != null){
if (cur.next.val == key){
return cur;
}
cur = cur.next;
}
return null;
}
7.删除key第一次出现的节点
1.判断链表是否为空,为空则不满足要求
2.如头节点的val为关键字key,则将head引用指向下一节点。
3.如果目标key在链表中间,找到前驱后改变指向,删除节点。
//删除第一次出现关键字为key的节点
public void remove(int key){
if (this.head == null){
System.out.println("链表为空,不能删除");
return;
}
if (this.head.val == key){
this.head = this.head.next;
return;
}
ListNode prev = searchPre(key);
prev.next = prev.next.next;
}
带傀儡节点双向非循环链表组成及定义
组成:
节点同next和prev实现链接,head指向头节点,last指向尾节点。
同样将节点定义为一个类:
class ListNode{
public int val;
public ListNode prev;
public ListNode next;
public ListNode(int data){
this.val = data;
}
}
定义头引用和尾引用定义链表的构造方法(构造傀儡节点)
public ListNode head;
public ListNode last;
public DoubleLinkedList(){
this.head = new ListNode(-1);
}
带傀儡节点双向非循环链表接口的实现
1、打印链表
等同与单向链表的打印
1.定义一个引用指向头节点的下一节点
2.逐个遍历节点打印其val值
public void display(){
ListNode cur = this.head;
while (cur != null){
System.out.println(cur.val);
cur = cur.next;
}
}
2、获取双向链表的长度
1.定义一个cur引用指向头节点的下一节点;定义一个计数变量count赋初值为0;
2.将cur逐个向后遍历,count++,后放回count的值
public int size(){
ListNode cur = this.head;
int count = 0;
while (cur != null){
cur = cur.next;
count++;
}
return count;
}
3、查找链表中是否包含关键字为key的节点
1.定义一个移动引用cur指向头节点的下一节点;
2.循环使cur逐个移动,判断cur指向的节点的val是否和key相等;
3.相等返回true,不相等放回false.
public boolean contain(int key){
ListNode cur = this.head.next;
while (size() != 0){
if (cur.val == key){
return true;
}
cur = cur.next;
}
return false;
}
4、头插法
1.用关键字new实例化一个节点
2.判断此时链表是否只要傀儡节点
① 若只有傀儡节点,改变节点的指向,尾巴引用应该指向插入节点。
② 直接改变节点的指向,使其链接在头节点和之和。
public void addFirst(int data){
ListNode node = new ListNode(data);
if (this.head != null && this.head.next == null){
this.head.next =node;
node.prev = this.head;
this.last = node;
}else {
node.next = this.head.next;
node.prev = this.head;
this.head.next = node;
}
}
4、尾插法
1.用关键字new实例化一个节点
2.判断此时链表是否只要傀儡节点
① 若只有傀儡节点,改变节点的指向,尾巴引用应该指向插入节点。
② 直接改变节点的指向,使其链接在最后。
public void addLast(int data){
ListNode node = new ListNode(data);
if (this.head.next == null && this.last == null){
this.head.next =node;
node.prev = this.head;
this.last = node;
return;
}else {
this.last.next = node;
node.prev = this.last;
this.last = node;
}
}
5、往index位置插入节点
1.判断插入位置是否合法
2.对插入位置的不同提供合适的插入方法
① 当插入位置为0时,采用头插法
② 当插入位置为链表的长度时,采用尾插法
③当插入位置在中间时,定义一个cur引用指向头节点,向后移动index步;再改变所指节点的指向,插入节点。
public void addIndex(int index,int data){
if (index < 0 || index > size()){
System.out.println("插入位置不合法");
return;
}
if (index == 0){
addFirst(data);
return;
}
if (index == size()){
addFirst(data);
return;
}else {
ListNode cur = this.head;
while (index != 0){
cur = cur.next;
index--;
}
ListNode node = new ListNode(data);
node.next = cur.next.next;
cur.next.prev = node;
cur.next = node;
node.prev = cur;
}
}
6、删除第一次出现的关键字为key的节点
判断过程:
1.先判断是否为删除第一个节点。
2.若不是删除第一个节点,则在判断删除的位置是中间还是最后。
3.因为只删除第一次出现关键字key的节点,所以删完后便return结束循环
public void removeKey(int key){
ListNode cur = this.head.next;
while (cur != null){
if (cur.val == key){
if (cur == this.head.next){
this.head.next = cur.next;
cur.next.prev = this.head;
}else {
if (cur.next != null){
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
}else {
cur.prev.next = cur.next;
this.last = this.last.prev;
}
}
return;
}
cur = cur.next;
}
}
7、删除所有关键字为key的节点
判断过程:
1.先判断是否为删除第一个节点。
2.若不是删除第一个节点,则在判断删除的位置是中间还是最后。
3.因为要删除所有关键字为key的节点,所以删除完一次后不需要结束,而是继续向后走,直至遇到表尾。
public void removeKey(int key){
ListNode cur = this.head.next;
while (cur != null){
if (cur.val == key){
if (cur == this.head.next){
this.head.next = cur.next;
cur.next.prev = this.head;
}else {
if (cur.next != null){
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
}else {
cur.prev.next = cur.next;
this.last = this.last.prev;
}
}
}
cur = cur.next;
}
}