數據結構與算法
1. 什麼是數據結構?
計算機組織與存儲數據的邏輯結構,目的是爲了實現高效的數據訪問與修改。
常見的數據結構有數組、鏈表、隊列、棧、二叉樹、散列表、圖等等。
2.什麼是算法?
爲了解決特點的問題,對數據進行加工的步驟
常見的算法有排序、查找
算法一般包含了輸入、輸出、有限的步驟、確定性
3. 數據結構與算法的關係?
程序 = 數據結構 + 算法
注:任何程序說到底都是對數據進行加工與處理,這個加工與處理所涉及的步驟就是算法,
而爲了對這些數據實現高效的訪問和修改,就需要使用相應的數據結構
雙向循環鏈表
數組:
優點:可以利用下標快速的找到某個元素!
缺點:插入,刪除元素時需要對整個數組進行調整
鏈表:
優點:插入,刪除元素非常快
缺點:查找慢,只能順序查找
除此以外,數組長度固定、需要連續的地址空間,而鏈表長度不固定,不需要連續的地址空間
- 什麼是雙向循環鏈表
雙向循環鏈表由一系列節點構成,其中每個節點包含兩部分內容,一個是元素(即數據),另外一個是指向前驅節點和後繼節點的指針。另外,頭節點和尾節點也彼此指向。
注:JDK8之前,LinkedList內部採用的雙向循環鏈表來實現的,JDK8之後改成了雙向鏈表來實現。
搭建雙向循環鏈表的結構
package list;
/**
* 雙向循環鏈表
*/
public class LinkedList<E> {
//雙向循環鏈表的頭
private Node head;
public boolean add(E e) {
//
}
/**
* 內部節點類(將元素封裝成節點的類)
*/
class Node{
E data; // 存放的數據
Node next; // 後繼節點
Node prev; // 前驅節點
Node(E e){
data = e;
}
}
}
add(E e)
方法
添加元素到鏈表的末尾
添加成功則返回true,添加失敗則返回false
package list;
/**
* 雙向循環鏈表
*/
public class LinkedList<E> {
//雙向循環鏈表的頭
private Node head;
/**
* 將一個元素添加到鏈表的末尾
* @param e 被添加的元素
* @return 添加成功,返回true
*/
public boolean add(E e) {
// 將元素封裝成節點
Node node = new Node(e);
// 查看當前鏈表是否爲空
if(head == null) {
head = node;
// 如果當前只有這一個節點,則它的前驅結點跟後繼節點都指向它自己
head.next = node;
head.prev = node;
return true;
}
// 如果鏈表不爲空,則先找到尾節點
Node last = head.prev;
// 將新節點添加進來(重新建立新的引用關係)
last.next = node;
node.next = head;
head.prev = node;
node.prev = last;
return true;
}
/**
* 內部節點類(將元素封裝成節點的類)
*/
class Node{
E data; // 存放的數據
Node next; // 後繼節點
Node prev; // 前驅節點
Node(E e){
data = e;
}
}
}
toString
方法
爲了方便查看雙向循環鏈表的數據
如果頭節點爲空,則沒有數據,輸出[]
否則依次遍歷鏈表,當遍歷的節點==頭節點時結束循環
@Override
public String toString() {
if(head == null) {
return "[]";
}
StringBuilder sb = new StringBuilder("[");
sb.append(head.data);
Node node = head.next;
while(node != head) {
sb.append(","+node.data);
node = node.next;
}
sb.append("]");
return sb.toString();
}
int size()
方法
返回雙向循環鏈表的長度
/**
* 返回節點的個數,雙向循環鏈表的長度
* @return 雙向循環鏈表的長度
*/
public int size() {
if(head == null) {
return 0;
}
int size = 1;
Node node = head.next;
while(node != head) {
size += 1;
node = node.next;
}
return size;
}
注意:上面這種做法要遍歷鏈表,如果鏈表特別長的話,耗時,可以進行優化
設置一個全局屬性size,每次做add(E e)操作時,size加1;每次做remove操作時,size減1
package list;
/**
* 雙向循環鏈表
*/
public class LinkedList<E> {
//雙向循環鏈表的頭
private Node head;
// 存放鏈表的長度(節點的格式)
private int size;
/**
* 將一個元素添加到鏈表的末尾
* @param e 被添加的元素
* @return 添加成功,返回true
*/
public boolean add(E e) {
// 將元素封裝成節點
Node node = new Node(e);
// 查看當前鏈表是否爲空
if(head == null) {
head = node;
// 如果當前只有這一個節點,則它的前驅結點跟後繼節點都指向它自己
head.next = node;
head.prev = node;
// 將鏈表中節點的個數加1
size++;
return true;
}
// 如果鏈表不爲空,則先找到尾節點
Node last = head.prev;
// 將新節點添加進來(重新建立新的引用關係)
last.next = node;
node.next = head;
head.prev = node;
node.prev = last;
// 將鏈表中節點的個數加1
size++;
return true;
}
/**
* 返回鏈表當中節點的個數
* @return 鏈表的長度
*/
public int size() {
return size;
}
/**
* 內部節點類(將元素封裝成節點的類)
*/
class Node{
E data; // 存放的數據
Node next; // 後繼節點
Node prev; // 前驅節點
Node(E e){
data = e;
}
}
}
get(int index)
方法
返回指定下標的元素
/**
* 返回指定下標的元素
* @param index 下標位置
* @return 元素
*/
public E get(int index) {
if(index < 0 || index >= size) {
throw new IndexOutOfBoundsException("下標越界!");
}
Node node = head;
for(int i=0; i<index; i++) {
node = node.next;
}
return node.data;
}
注意:這麼寫就夠了。如果數量特別大,下標位置又比較靠後,比如遍歷倒數第二個,則順序遍歷耗時
可以進一步優化
可以用二分法進行查找,進行反向查詢
/**
* 返回指定下標的元素
* @param index 下標位置
* @return 元素
*/
public E get(int index) {
if(index < 0 || index >= size) {
throw new IndexOutOfBoundsException("下標越界!");
}
Node node = head;
if(index < size/2) {
for(int i=0; i<index; i++) {
node = node.next;
}
}else {
for(int i=size; i>index; i--) {
node = node.prev;
}
}
return node.data;
}
由於刪除操作也是要找到對應位置的節點。所以可以把上面找到位置的節點代碼封裝成一個小方法
/**
* 返回指定下標的元素
* @param index 下標位置
* @return 元素
*/
public E get(int index) {
if(index < 0 || index >= size) {
throw new IndexOutOfBoundsException("下標越界!");
}
Node node = getNode(index);
return node.data;
}
/**
* 返回指定下標出的節點
*/
private Node getNode(int index) {
Node node = head;
if(index < size/2) {
for(int i=0; i<index; i++) {
node = node.next;
}
}else {
for(int i=size; i>index; i--) {
node = node.prev;
}
}
return node;
}
remove(int index)
方法
刪除指定下標的節點
返回被刪除的節點元素
注意下標越界的情況
注意整個鏈表只有一個節點的情況(刪掉就爲null了)
注意如果把頭節點刪掉了,要重新指定新的頭節點
注意刪除後size要減1
分析:
例如:[8, 22, 16, 11, 33]
可以用getNode(index)方法得到要刪除的那個節點node(比如16)
node.next得到後繼節點(11)
node.prev得到前驅節點(22)
重新建立連接(11和22重新建立節點連接)
/**
* 刪除指定下標的節點
* @param index 下標位置
* @return 被刪除的節點的元素
*/
public E remove(int index) {
if(index < 0 || index >= size) {
throw new IndexOutOfBoundsException("下標越界!");
}
// 考慮鏈表長度爲1的情況
if(size == 1) {
// 長度爲1,則只有head這一個節點,刪除直接設置爲null即可
E data = head.data;
head = null;
size--;
return data;
}
// 找到要刪除的節點
Node node = getNode(index);
// 找到其前驅節點和後繼節點
Node nodePrev = node.prev;
Node nodeNext = node.next;
// 重新建立節點連接
nodeNext.prev = nodePrev;
nodePrev.next = nodeNext;
// 考慮刪除頭節點的情況,則要將下一個節點設置爲頭節點
if(index == 0) {
head = nodeNext;
}
// 鏈表的長度減1
size--;
// 返回被刪除的節點元素
return node.data;
}
add(int index, E e)
方法
在指定位置上插入元素
用getNode(index)方法得到下標爲index的那個節點node(比如16)
找到其前驅節點prev(22)
重新進行節點關聯
注意特殊情況(index=0),則頭節點改變了
/**
* 在指定位置上插入元素
* @param index 下標位置
* @param e 要插入的元素
*/
public void add(int index, E e) {
if(index < 0 || index >= size) {
throw new IndexOutOfBoundsException("下標越界!");
}
// 封裝要插入的元素節點
Node node = new Node(e);
// 先找到下標爲index的節點
Node nodeNext = getNode(index);
// 找到其前驅結點
Node nodePrev = nodeNext.prev;
// 重新進行節點的關聯
nodeNext.prev = node;
node.prev = nodePrev;
nodePrev.next = node;
node.next = nodeNext;
if(index == 0) {
head = node;
}
}
完整代碼如下:
package list;
/**
* 雙向循環鏈表
*/
public class LinkedList<E> {
//雙向循環鏈表的頭
private Node head;
// 存放鏈表的長度(節點的格式)
private int size;
/**
* 將一個元素添加到鏈表的末尾
* @param e 被添加的元素
* @return 添加成功,返回true
*/
public boolean add(E e) {
// 將元素封裝成節點
Node node = new Node(e);
// 查看當前鏈表是否爲空
if(head == null) {
head = node;
// 如果當前只有這一個節點,則它的前驅結點跟後繼節點都指向它自己
head.next = node;
head.prev = node;
// 將鏈表中節點的個數加1
size++;
return true;
}
// 如果鏈表不爲空,則先找到尾節點
Node last = head.prev;
// 將新節點添加進來(重新建立新的引用關係)
last.next = node;
node.next = head;
head.prev = node;
node.prev = last;
// 將鏈表中節點的個數加1
size++;
return true;
}
/**
* 返回鏈表當中節點的個數
* @return 鏈表的長度
*/
public int size() {
return size;
}
/**
* 返回指定下標的元素
* @param index 下標位置
* @return 元素
*/
public E get(int index) {
if(index < 0 || index >= size) {
throw new IndexOutOfBoundsException("下標越界!");
}
Node node = getNode(index);
return node.data;
}
/**
* 刪除指定下標的節點
* @param index 下標位置
* @return 被刪除的節點的元素
*/
public E remove(int index) {
if(index < 0 || index >= size) {
throw new IndexOutOfBoundsException("下標越界!");
}
// 考慮鏈表長度爲1的情況
if(size == 1) {
// 長度爲1,則只有head這一個節點,刪除直接設置爲null即可
E data = head.data;
head = null;
size--;
return data;
}
// 找到要刪除的節點
Node node = getNode(index);
// 找到其前驅節點和後繼節點
Node nodePrev = node.prev;
Node nodeNext = node.next;
// 重新建立節點連接
nodeNext.prev = nodePrev;
nodePrev.next = nodeNext;
// 考慮刪除頭節點的情況,則要將下一個節點設置爲頭節點
if(index == 0) {
head = nodeNext;
}
// 鏈表的長度減1
size--;
// 返回被刪除的節點元素
return node.data;
}
/**
* 在指定位置上插入元素
* @param index 下標位置
* @param e 要插入的元素
*/
public void add(int index, E e) {
if(index < 0 || index >= size) {
throw new IndexOutOfBoundsException("下標越界!");
}
// 封裝要插入的元素節點
Node node = new Node(e);
// 先找到下標爲index的節點
Node nodeNext = getNode(index);
// 找到其前驅結點
Node nodePrev = nodeNext.prev;
// 重新進行節點的關聯
nodeNext.prev = node;
node.prev = nodePrev;
nodePrev.next = node;
node.next = nodeNext;
if(index == 0) {
head = node;
}
}
/**
* 返回指定下標出的節點
*/
private Node getNode(int index) {
Node node = head;
if(index < size/2) {
for(int i=0; i<index; i++) {
node = node.next;
}
}else {
for(int i=size; i>index; i--) {
node = node.prev;
}
}
return node;
}
@Override
public String toString() {
if(head == null) {
return "[]";
}
StringBuilder sb = new StringBuilder("[");
sb.append(head.data);
Node node = head.next;
while(node != head) {
sb.append(","+node.data);
node = node.next;
}
sb.append("]");
return sb.toString();
}
/**
* 內部節點類(將元素封裝成節點的類)
*/
class Node{
E data; // 存放的數據
Node next; // 後繼節點
Node prev; // 前驅節點
Node(E e){
data = e;
}
}
}